feat:首页完成
@@ -5,14 +5,18 @@ const props = defineProps<{
|
||||
active: TabKey
|
||||
}>()
|
||||
|
||||
const tabs: Array<{ key: TabKey, label: string, icon: string, path: string }> = [
|
||||
{ key: 'home', label: '首页', icon: '/static/icons/tab-home.svg', path: '/pages/home-index/index' },
|
||||
{ key: 'case', label: '案例库', icon: '/static/icons/tab-case.svg', path: '/pages/case-library/index' },
|
||||
{ key: 'message', label: '消息中心', icon: '/static/icons/tab-message.svg', path: '/pages/message-center/index' },
|
||||
{ key: 'project', label: '项目中心', icon: '/static/icons/tab-project.svg', path: '/pages/project-center/index' },
|
||||
{ key: 'personal', label: '个人中心', icon: '/static/icons/tab-personal.svg', path: '/pages/personal-center/index' },
|
||||
const tabs: Array<{ key: TabKey, label: string, icon: string, activeIcon: string, path: string }> = [
|
||||
{ key: 'home', label: '首页', icon: '/static/icons/tab-home.png', activeIcon: '/static/icons/tab-home-active.png', path: '/pages/home-index/index' },
|
||||
{ key: 'case', label: '案例库', icon: '/static/icons/tab-case.png', activeIcon: '/static/icons/tab-case-active.png', path: '/pages/case-library/index' },
|
||||
{ key: 'message', label: '消息中心', icon: '/static/icons/tab-message.png', activeIcon: '/static/icons/tab-message-active.png', path: '/pages/message-center/index' },
|
||||
{ key: 'project', label: '项目中心', icon: '/static/icons/tab-project.png', activeIcon: '/static/icons/tab-project-active.png', path: '/pages/project-center/index' },
|
||||
{ key: 'personal', label: '个人中心', icon: '/static/icons/tab-personal.png', activeIcon: '/static/icons/tab-personal-active.png', path: '/pages/personal-center/index' },
|
||||
]
|
||||
|
||||
function getTabIcon(tab: typeof tabs[number]) {
|
||||
return tab.key === props.active ? tab.activeIcon : tab.icon
|
||||
}
|
||||
|
||||
function handleSwitch(path: string, key: TabKey) {
|
||||
if (key === props.active)
|
||||
return
|
||||
@@ -30,8 +34,7 @@ function handleSwitch(path: string, key: TabKey) {
|
||||
>
|
||||
<image
|
||||
class="app-tab-bar__icon"
|
||||
:class="{ 'app-tab-bar__icon--active': tab.key === active }"
|
||||
:src="tab.icon"
|
||||
:src="getTabIcon(tab)"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text
|
||||
@@ -71,11 +74,6 @@ function handleSwitch(path: string, key: TabKey) {
|
||||
.app-tab-bar__icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
opacity: 0.45;
|
||||
|
||||
&--active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.app-tab-bar__label {
|
||||
|
||||
34
src/constants/business-detail.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { BusinessDetailContent } from '@/types/business-detail'
|
||||
import softwareIllus from '@/static/icons/software.png'
|
||||
|
||||
export const BUSINESS_DETAIL_MAP: Record<string, BusinessDetailContent> = {
|
||||
software: {
|
||||
id: 'software',
|
||||
title: '互联网软件',
|
||||
subtitle: '专业的软件定制开发服务,从需求到上线全流程保障',
|
||||
icon: softwareIllus,
|
||||
serviceTypes: [
|
||||
{ id: 'web', label: 'Web应用', icon: '/static/icons/service-web.png' },
|
||||
{ id: 'mini', label: '小程序', icon: '/static/icons/service-mini.png' },
|
||||
{ id: 'app', label: 'APP', icon: '/static/icons/service-app.png' },
|
||||
{ id: 'secondary', label: '二次开发', icon: '/static/icons/service-secondary.png' },
|
||||
{ id: 'desktop', label: '桌面工具', icon: '/static/icons/service-desktop.png' },
|
||||
],
|
||||
contentTitle: '服务内容',
|
||||
contentSections: [
|
||||
'一、产品咨询与规划服务',
|
||||
'1. 业务诊断与梳理:深入了解客户业务现状,梳理核心流程与痛点,输出业务诊断报告。',
|
||||
'2. 竞品分析与市场调研:分析行业竞品功能与商业模式,为产品定位提供数据支撑。',
|
||||
'3. 产品定位与MVP规划:明确产品核心价值,制定最小可行产品(MVP)路线图。',
|
||||
'4. 技术可行性评估:评估技术实现路径、开发周期与成本,降低项目风险。',
|
||||
'二、UI/UX 设计服务',
|
||||
'1. 用户体验研究:通过用户访谈、可用性测试等方法,洞察用户需求与行为。',
|
||||
'2. 交互原型设计:输出高保真交互原型,确保产品逻辑清晰、操作流畅。',
|
||||
'3. 视觉界面设计:打造统一、美观、符合品牌调性的视觉设计体系。',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export function getBusinessDetail(id: string) {
|
||||
return BUSINESS_DETAIL_MAP[id]
|
||||
}
|
||||
13
src/constants/business-types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const BUSINESS_TYPE_OPTIONS = [
|
||||
'互联网软件',
|
||||
'硬件电路',
|
||||
'物联网全案',
|
||||
'生产制造',
|
||||
]
|
||||
|
||||
export const BUSINESS_TYPE_DETAIL_ID: Record<string, string> = {
|
||||
互联网软件: 'software',
|
||||
硬件电路: 'hardware',
|
||||
物联网全案: 'iot',
|
||||
生产制造: 'production',
|
||||
}
|
||||
126
src/constants/case-library.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { CaseCategory, CaseFilterValues, CaseItem } from '@/types/case-library'
|
||||
|
||||
export const CASE_CATEGORIES: CaseCategory[] = [
|
||||
'全部',
|
||||
'互联网软件',
|
||||
'硬件电路',
|
||||
'物联网全案',
|
||||
'生产制造',
|
||||
]
|
||||
|
||||
export const CASE_PAGE_SIZE = 4
|
||||
|
||||
export const CASE_TOTAL_COUNT = 6
|
||||
|
||||
export const CASE_FILTER_INDUSTRIES = [
|
||||
'金融科技',
|
||||
'电商零售',
|
||||
'教育培训',
|
||||
'医疗健康',
|
||||
'硬件电路',
|
||||
'能源环保',
|
||||
'房地产',
|
||||
] as const
|
||||
|
||||
export const CASE_FILTER_SCALES = [
|
||||
'全部',
|
||||
'50万以下',
|
||||
'50-100万',
|
||||
'100-300万',
|
||||
'300-500万',
|
||||
'500万以上',
|
||||
] as const
|
||||
|
||||
export const CASE_FILTER_TIME_RANGES = [
|
||||
'全部',
|
||||
'近3个月',
|
||||
'近半年',
|
||||
'近一年',
|
||||
] as const
|
||||
|
||||
export const DEFAULT_CASE_FILTER: CaseFilterValues = {
|
||||
industries: [],
|
||||
scales: [],
|
||||
timeRanges: [],
|
||||
dateRange: {
|
||||
start: '',
|
||||
end: '',
|
||||
},
|
||||
}
|
||||
|
||||
export const CASE_LIBRARY_LIST: CaseItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: '智能园区综合管理平台开发',
|
||||
category: '互联网软件',
|
||||
cover: '/static/icons/background.png',
|
||||
clientType: '企业',
|
||||
industry: '房地产',
|
||||
scale: '200万',
|
||||
cycle: '200个工作日',
|
||||
teamSize: '20人',
|
||||
completedAt: '2026/8/10',
|
||||
favorite: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '工业设备远程监控系统',
|
||||
category: '物联网全案',
|
||||
cover: '/static/icons/1.png',
|
||||
clientType: '企业',
|
||||
industry: '金融科技',
|
||||
scale: '150万',
|
||||
cycle: '180个工作日',
|
||||
teamSize: '16人',
|
||||
completedAt: '2026/7/22',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: '社区康养智能服务系统',
|
||||
category: '互联网软件',
|
||||
cover: '/static/icons/software.png',
|
||||
clientType: '政府',
|
||||
industry: '医疗健康',
|
||||
scale: '320万',
|
||||
cycle: '240个工作日',
|
||||
teamSize: '24人',
|
||||
completedAt: '2026/6/15',
|
||||
favorite: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: '新能源充电桩控制主板设计',
|
||||
category: '硬件电路',
|
||||
cover: '/static/icons/hardware.png',
|
||||
clientType: '企业',
|
||||
industry: '能源环保',
|
||||
scale: '80万',
|
||||
cycle: '90个工作日',
|
||||
teamSize: '8人',
|
||||
completedAt: '2026/5/28',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: '智慧农业环境监测平台',
|
||||
category: '物联网全案',
|
||||
cover: '/static/icons/iot.png',
|
||||
clientType: '企业',
|
||||
industry: '教育培训',
|
||||
scale: '260万',
|
||||
cycle: '210个工作日',
|
||||
teamSize: '18人',
|
||||
completedAt: '2026/4/12',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: '智能穿戴设备批量生产项目',
|
||||
category: '生产制造',
|
||||
cover: '/static/icons/production.png',
|
||||
clientType: '企业',
|
||||
industry: '电商零售',
|
||||
scale: '500万',
|
||||
cycle: '300个工作日',
|
||||
teamSize: '30人',
|
||||
completedAt: '2026/3/8',
|
||||
},
|
||||
]
|
||||
41
src/constants/quick-entries.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { QuickEntryItem } from '@/types/home'
|
||||
|
||||
export const QUICK_ENTRY_MAX = 5
|
||||
|
||||
export const QUICK_ENTRY_STORAGE_KEY = 'home-quick-entry-ids'
|
||||
|
||||
export const DEFAULT_QUICK_ENTRY_IDS = [
|
||||
'project',
|
||||
'report',
|
||||
'service',
|
||||
'contract',
|
||||
'finance',
|
||||
]
|
||||
|
||||
export const QUICK_ENTRY_CATALOG: QuickEntryItem[] = [
|
||||
{ id: 'project', label: '项目中心', icon: '/static/icons/quick-project.png', bgColor: '#E8F0FF' },
|
||||
{ id: 'report', label: '快捷提报', icon: '/static/icons/quick-report.png', bgColor: '#E8FAF4' },
|
||||
{ id: 'service', label: '在线客服', icon: '/static/icons/quick-service.png', bgColor: '#F3E8FF' },
|
||||
{ id: 'contract', label: '合同管理', icon: '/static/icons/quick-contract.png', bgColor: '#FEE2E2' },
|
||||
{ id: 'finance', label: '财务管理', icon: '/static/icons/quick-finance.png', bgColor: '#E0F2FE' },
|
||||
{ id: 'personal', label: '个人中心', icon: '/static/icons/tab-personals.png', bgColor: '#E8F0FF' },
|
||||
{ id: 'message', label: '消息中心', icon: '/static/icons/tab-messages.png', bgColor: '#F3E8FF' },
|
||||
{ id: 'security', label: '账号安全', icon: '/static/icons/security.png', bgColor: '#E8FAF4' },
|
||||
{ id: 'feedback', label: '意见反馈', icon: '/static/icons/feedback.png', bgColor: '#FFECE1' },
|
||||
{ id: 'contract-list', label: '合同列表', icon: '/static/icons/contract-list.png', bgColor: '#FEE2E2' },
|
||||
{ id: 'invoice', label: '发票列表', icon: '/static/icons/invoice.png', bgColor: '#E0F2FE' },
|
||||
{ id: 'address', label: '地址管理', icon: '/static/icons/address.png', bgColor: '#E8F0FF' },
|
||||
{ id: 'qualification', label: '资质列表', icon: '/static/icons/qualification.png', bgColor: '#FFF3EB' },
|
||||
{ id: 'help', label: '帮助中心', icon: '/static/icons/help.png', bgColor: '#F3E8FF' },
|
||||
{ id: 'about', label: '关于我们', icon: '/static/icons/about.png', bgColor: '#E8F0FF' },
|
||||
]
|
||||
|
||||
export function getQuickEntryById(id: string) {
|
||||
return QUICK_ENTRY_CATALOG.find(item => item.id === id)
|
||||
}
|
||||
|
||||
export function mapQuickEntryIds(ids: string[]) {
|
||||
return ids
|
||||
.map(id => getQuickEntryById(id))
|
||||
.filter((item): item is QuickEntryItem => Boolean(item))
|
||||
}
|
||||
71
src/hooks/use-quick-entries.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
DEFAULT_QUICK_ENTRY_IDS,
|
||||
mapQuickEntryIds,
|
||||
QUICK_ENTRY_CATALOG,
|
||||
QUICK_ENTRY_MAX,
|
||||
QUICK_ENTRY_STORAGE_KEY,
|
||||
} from '@/constants/quick-entries'
|
||||
|
||||
function loadPinnedIds(): string[] {
|
||||
try {
|
||||
const stored = uni.getStorageSync(QUICK_ENTRY_STORAGE_KEY)
|
||||
if (Array.isArray(stored) && stored.length > 0)
|
||||
return stored.slice(0, QUICK_ENTRY_MAX)
|
||||
}
|
||||
catch {
|
||||
// ignore storage read errors
|
||||
}
|
||||
return [...DEFAULT_QUICK_ENTRY_IDS]
|
||||
}
|
||||
|
||||
function savePinnedIds(ids: string[]) {
|
||||
uni.setStorageSync(QUICK_ENTRY_STORAGE_KEY, ids.slice(0, QUICK_ENTRY_MAX))
|
||||
}
|
||||
|
||||
export function useQuickEntries() {
|
||||
const pinnedIds = ref<string[]>(loadPinnedIds())
|
||||
const editorVisible = ref(false)
|
||||
|
||||
const pinnedEntries = computed(() => mapQuickEntryIds(pinnedIds.value))
|
||||
|
||||
const otherEntries = computed(() =>
|
||||
QUICK_ENTRY_CATALOG.filter(item => !pinnedIds.value.includes(item.id)),
|
||||
)
|
||||
|
||||
function openEditor() {
|
||||
editorVisible.value = true
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
editorVisible.value = false
|
||||
}
|
||||
|
||||
function removeFromPinned(id: string) {
|
||||
pinnedIds.value = pinnedIds.value.filter(itemId => itemId !== id)
|
||||
savePinnedIds(pinnedIds.value)
|
||||
}
|
||||
|
||||
function addToPinned(id: string) {
|
||||
if (pinnedIds.value.includes(id))
|
||||
return
|
||||
|
||||
if (pinnedIds.value.length >= QUICK_ENTRY_MAX) {
|
||||
uni.showToast({ title: `最多添加${QUICK_ENTRY_MAX}个快捷入口`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
pinnedIds.value = [...pinnedIds.value, id]
|
||||
savePinnedIds(pinnedIds.value)
|
||||
}
|
||||
|
||||
return {
|
||||
pinnedEntries,
|
||||
otherEntries,
|
||||
editorVisible,
|
||||
openEditor,
|
||||
closeEditor,
|
||||
removeFromPinned,
|
||||
addToPinned,
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,18 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/home-index/business-detail/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/home-index/requirement-submit/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/case-library/index",
|
||||
"style": {
|
||||
|
||||
@@ -25,6 +25,10 @@ onLoad((options) => {
|
||||
}
|
||||
if (type === 'loginFailed') {
|
||||
pageType.value = 'loginFailed'
|
||||
return
|
||||
}
|
||||
if (type === 'submitSuccess') {
|
||||
pageType.value = 'submitSuccess'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
185
src/pages/case-library/components/CaseCard.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import type { CaseItem } from '@/types/case-library'
|
||||
|
||||
defineProps<{
|
||||
item: CaseItem
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleFavorite: [id: string]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="case-card">
|
||||
<view class="case-card__cover-wrap">
|
||||
<image
|
||||
class="case-card__cover"
|
||||
:src="item.cover"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<text class="case-card__tag">
|
||||
{{ item.category }}
|
||||
</text>
|
||||
<view
|
||||
class="case-card__favorite"
|
||||
@tap.stop="emit('toggleFavorite', item.id)"
|
||||
>
|
||||
<image
|
||||
class="case-card__favorite-icon"
|
||||
:src="item.favorite ? '/static/icons/icon-heart-filled.svg' : '/static/icons/icon-heart.svg'"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="case-card__title">
|
||||
{{ item.title }}
|
||||
</text>
|
||||
|
||||
<view class="case-card__meta">
|
||||
<view class="case-card__meta-row">
|
||||
<view class="case-card__meta-item">
|
||||
<text class="case-card__meta-label">
|
||||
客户类型
|
||||
</text>
|
||||
<text class="case-card__meta-value">
|
||||
{{ item.clientType }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="case-card__meta-item">
|
||||
<text class="case-card__meta-label">
|
||||
所属行业
|
||||
</text>
|
||||
<text class="case-card__meta-value">
|
||||
{{ item.industry }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="case-card__meta-item">
|
||||
<text class="case-card__meta-label">
|
||||
规模
|
||||
</text>
|
||||
<text class="case-card__meta-value">
|
||||
{{ item.scale }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="case-card__meta-row">
|
||||
<view class="case-card__meta-item">
|
||||
<text class="case-card__meta-label">
|
||||
项目周期
|
||||
</text>
|
||||
<text class="case-card__meta-value">
|
||||
{{ item.cycle }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="case-card__meta-item">
|
||||
<text class="case-card__meta-label">
|
||||
服务团队
|
||||
</text>
|
||||
<text class="case-card__meta-value">
|
||||
{{ item.teamSize }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="case-card__meta-item">
|
||||
<text class="case-card__meta-label">
|
||||
完成时间
|
||||
</text>
|
||||
<text class="case-card__meta-value">
|
||||
{{ item.completedAt }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.case-card {
|
||||
padding: 0 0 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
background: $color-bg-page;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.case-card__cover-wrap {
|
||||
position: relative;
|
||||
height: 320rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.case-card__cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.case-card__tag {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
left: 20rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: #ffffff;
|
||||
background: $color-primary;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.case-card__favorite {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.case-card__favorite-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
.case-card__title {
|
||||
display: block;
|
||||
padding: 24rpx 24rpx 0;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.case-card__meta {
|
||||
padding: 20rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.case-card__meta-row {
|
||||
display: flex;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.case-card__meta-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.case-card__meta-label {
|
||||
font-size: 22rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.case-card__meta-value {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
</style>
|
||||
526
src/pages/case-library/components/CaseFilterPanel.vue
Normal file
@@ -0,0 +1,526 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import {
|
||||
CASE_FILTER_INDUSTRIES,
|
||||
CASE_FILTER_SCALES,
|
||||
CASE_FILTER_TIME_RANGES,
|
||||
DEFAULT_CASE_FILTER,
|
||||
} from '@/constants/case-library'
|
||||
import type { CaseFilterValues, CaseItem } from '@/types/case-library'
|
||||
import {
|
||||
cloneCaseFilter,
|
||||
getSelectedFilterTags,
|
||||
matchCaseFilter,
|
||||
removeFilterTag,
|
||||
} from '@/utils/case-filter'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
cases: CaseItem[]
|
||||
value: CaseFilterValues
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
confirm: [value: CaseFilterValues]
|
||||
}>()
|
||||
|
||||
const draft = ref<CaseFilterValues>(cloneCaseFilter(DEFAULT_CASE_FILTER))
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible)
|
||||
draft.value = cloneCaseFilter(props.value)
|
||||
},
|
||||
)
|
||||
|
||||
const selectedTags = computed(() => getSelectedFilterTags(draft.value))
|
||||
|
||||
const previewCount = computed(() => {
|
||||
return props.cases.filter(item => matchCaseFilter(item, draft.value)).length
|
||||
})
|
||||
|
||||
function isIndustryActive(item: string) {
|
||||
return draft.value.industries.includes(item)
|
||||
}
|
||||
|
||||
function isScaleActive(item: string) {
|
||||
if (item === '全部')
|
||||
return draft.value.scales.length === 0
|
||||
|
||||
return draft.value.scales.includes(item)
|
||||
}
|
||||
|
||||
function isTimeActive(item: string) {
|
||||
if (item === '全部')
|
||||
return draft.value.timeRanges.length === 0 && !draft.value.dateRange.start && !draft.value.dateRange.end
|
||||
|
||||
return draft.value.timeRanges.includes(item)
|
||||
}
|
||||
|
||||
function toggleIndustry(item: string) {
|
||||
const next = cloneCaseFilter(draft.value)
|
||||
|
||||
if (next.industries.includes(item))
|
||||
next.industries = next.industries.filter(value => value !== item)
|
||||
else
|
||||
next.industries.push(item)
|
||||
|
||||
draft.value = next
|
||||
}
|
||||
|
||||
function toggleScale(item: string) {
|
||||
const next = cloneCaseFilter(draft.value)
|
||||
|
||||
if (item === '全部') {
|
||||
next.scales = []
|
||||
draft.value = next
|
||||
return
|
||||
}
|
||||
|
||||
if (next.scales.includes(item))
|
||||
next.scales = next.scales.filter(value => value !== item)
|
||||
else
|
||||
next.scales.push(item)
|
||||
|
||||
draft.value = next
|
||||
}
|
||||
|
||||
function toggleTime(item: string) {
|
||||
const next = cloneCaseFilter(draft.value)
|
||||
|
||||
if (item === '全部') {
|
||||
next.timeRanges = []
|
||||
next.dateRange = { start: '', end: '' }
|
||||
draft.value = next
|
||||
return
|
||||
}
|
||||
|
||||
if (next.timeRanges.includes(item))
|
||||
next.timeRanges = next.timeRanges.filter(value => value !== item)
|
||||
else
|
||||
next.timeRanges.push(item)
|
||||
|
||||
draft.value = next
|
||||
}
|
||||
|
||||
function handleDateChange(type: 'start' | 'end', value: string) {
|
||||
const next = cloneCaseFilter(draft.value)
|
||||
next.dateRange[type] = value.replace(/-/g, '/')
|
||||
next.timeRanges = []
|
||||
draft.value = next
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
draft.value = cloneCaseFilter(DEFAULT_CASE_FILTER)
|
||||
}
|
||||
|
||||
function handleClearTags() {
|
||||
draft.value = cloneCaseFilter(DEFAULT_CASE_FILTER)
|
||||
}
|
||||
|
||||
function handleRemoveTag(key: string) {
|
||||
draft.value = removeFilterTag(draft.value, key)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
emit('confirm', cloneCaseFilter(draft.value))
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
v-if="visible"
|
||||
class="case-filter-panel"
|
||||
>
|
||||
<view
|
||||
class="case-filter-panel__mask"
|
||||
@tap="handleCancel"
|
||||
/>
|
||||
|
||||
<view
|
||||
class="case-filter-panel__sheet"
|
||||
@tap.stop
|
||||
>
|
||||
<view class="case-filter-panel__header">
|
||||
<view class="case-filter-panel__title-wrap">
|
||||
<image
|
||||
class="case-filter-panel__title-icon"
|
||||
src="/static/icons/icon-filter.svg"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="case-filter-panel__title">
|
||||
筛选条件
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="case-filter-panel__reset"
|
||||
@tap="handleReset"
|
||||
>
|
||||
<text class="case-filter-panel__reset-icon">
|
||||
↺
|
||||
</text>
|
||||
<text class="case-filter-panel__reset-text">
|
||||
重置
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="case-filter-panel__body"
|
||||
scroll-y
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view class="case-filter-panel__section">
|
||||
<text class="case-filter-panel__section-title">
|
||||
行业
|
||||
</text>
|
||||
<view class="case-filter-panel__chips">
|
||||
<text
|
||||
v-for="item in CASE_FILTER_INDUSTRIES"
|
||||
:key="item"
|
||||
class="case-filter-panel__chip"
|
||||
:class="{ 'case-filter-panel__chip--active': isIndustryActive(item) }"
|
||||
@tap="toggleIndustry(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="case-filter-panel__section">
|
||||
<text class="case-filter-panel__section-title">
|
||||
项目规模
|
||||
</text>
|
||||
<view class="case-filter-panel__chips">
|
||||
<text
|
||||
v-for="item in CASE_FILTER_SCALES"
|
||||
:key="item"
|
||||
class="case-filter-panel__chip"
|
||||
:class="{ 'case-filter-panel__chip--active': isScaleActive(item) }"
|
||||
@tap="toggleScale(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="case-filter-panel__section">
|
||||
<text class="case-filter-panel__section-title">
|
||||
完成时间
|
||||
</text>
|
||||
<view class="case-filter-panel__chips">
|
||||
<text
|
||||
v-for="item in CASE_FILTER_TIME_RANGES"
|
||||
:key="item"
|
||||
class="case-filter-panel__chip"
|
||||
:class="{ 'case-filter-panel__chip--active': isTimeActive(item) }"
|
||||
@tap="toggleTime(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="case-filter-panel__date-row">
|
||||
<picker
|
||||
mode="date"
|
||||
:value="draft.dateRange.start.replace(/\//g, '-')"
|
||||
@change="handleDateChange('start', $event.detail.value)"
|
||||
>
|
||||
<view class="case-filter-panel__date-field">
|
||||
<text :class="{ 'case-filter-panel__date-placeholder': !draft.dateRange.start }">
|
||||
{{ draft.dateRange.start || '开始时间' }}
|
||||
</text>
|
||||
</view>
|
||||
</picker>
|
||||
<text class="case-filter-panel__date-separator">
|
||||
—
|
||||
</text>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="draft.dateRange.end.replace(/\//g, '-')"
|
||||
@change="handleDateChange('end', $event.detail.value)"
|
||||
>
|
||||
<view class="case-filter-panel__date-field">
|
||||
<text :class="{ 'case-filter-panel__date-placeholder': !draft.dateRange.end }">
|
||||
{{ draft.dateRange.end || '结束时间' }}
|
||||
</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view
|
||||
v-if="selectedTags.length > 0"
|
||||
class="case-filter-panel__selected"
|
||||
>
|
||||
<view class="case-filter-panel__selected-header">
|
||||
<text class="case-filter-panel__selected-title">
|
||||
已选条件 ({{ selectedTags.length }}个)
|
||||
</text>
|
||||
<text
|
||||
class="case-filter-panel__selected-clear"
|
||||
@tap="handleClearTags"
|
||||
>
|
||||
清空
|
||||
</text>
|
||||
</view>
|
||||
<view class="case-filter-panel__selected-tags">
|
||||
<view
|
||||
v-for="tag in selectedTags"
|
||||
:key="tag.key"
|
||||
class="case-filter-panel__tag"
|
||||
>
|
||||
<text class="case-filter-panel__tag-text">
|
||||
{{ tag.label }}
|
||||
</text>
|
||||
<text
|
||||
class="case-filter-panel__tag-close"
|
||||
@tap.stop="handleRemoveTag(tag.key)"
|
||||
>
|
||||
×
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="case-filter-panel__footer">
|
||||
<view
|
||||
class="case-filter-panel__btn case-filter-panel__btn--cancel"
|
||||
@tap="handleCancel"
|
||||
>
|
||||
取消
|
||||
</view>
|
||||
<view
|
||||
class="case-filter-panel__btn case-filter-panel__btn--confirm"
|
||||
@tap="handleConfirm"
|
||||
>
|
||||
确定 ({{ previewCount }}个结果)
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.case-filter-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 300;
|
||||
}
|
||||
|
||||
.case-filter-panel__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.case-filter-panel__sheet {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 78vh;
|
||||
background: $color-bg-page;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
}
|
||||
|
||||
.case-filter-panel__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
padding: 28rpx 32rpx 20rpx;
|
||||
}
|
||||
|
||||
.case-filter-panel__title-wrap,
|
||||
.case-filter-panel__reset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.case-filter-panel__title-icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.case-filter-panel__title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.case-filter-panel__reset-icon {
|
||||
margin-right: 6rpx;
|
||||
font-size: 28rpx;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.case-filter-panel__reset-text {
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.case-filter-panel__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.case-filter-panel__section {
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.case-filter-panel__section-title {
|
||||
display: block;
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.case-filter-panel__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.case-filter-panel__chip {
|
||||
padding: 14rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-primary;
|
||||
background: $color-bg-section;
|
||||
border-radius: 999rpx;
|
||||
|
||||
&--active {
|
||||
color: $color-primary;
|
||||
background: $color-primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
.case-filter-panel__date-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.case-filter-panel__date-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 220rpx;
|
||||
height: 72rpx;
|
||||
padding: 0 20rpx;
|
||||
background: $color-bg-section;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.case-filter-panel__date-placeholder {
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
|
||||
.case-filter-panel__date-separator {
|
||||
margin: 0 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.case-filter-panel__selected {
|
||||
flex-shrink: 0;
|
||||
margin: 0 32rpx 20rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
background: $color-bg-section;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.case-filter-panel__selected-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.case-filter-panel__selected-title {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.case-filter-panel__selected-clear {
|
||||
font-size: 24rpx;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.case-filter-panel__selected-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.case-filter-panel__tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 16rpx;
|
||||
background: $color-primary-light;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.case-filter-panel__tag-text {
|
||||
font-size: 22rpx;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.case-filter-panel__tag-close {
|
||||
margin-left: 8rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.case-filter-panel__footer {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.case-filter-panel__btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 88rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
|
||||
&--cancel {
|
||||
color: $color-primary;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid $color-primary;
|
||||
}
|
||||
|
||||
&--confirm {
|
||||
color: #ffffff;
|
||||
background: $color-primary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
83
src/pages/case-library/components/LoadMoreFooter.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
loading: boolean
|
||||
hasMore: boolean
|
||||
finished: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
loadMore: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
v-if="hasMore || finished"
|
||||
class="load-more-footer"
|
||||
@tap="emit('loadMore')"
|
||||
>
|
||||
<view
|
||||
v-if="loading"
|
||||
class="load-more-footer__loading"
|
||||
>
|
||||
<view class="load-more-footer__spinner" />
|
||||
<text class="load-more-footer__text">
|
||||
加载更多...
|
||||
</text>
|
||||
</view>
|
||||
<text
|
||||
v-else-if="hasMore"
|
||||
class="load-more-footer__text"
|
||||
>
|
||||
加载更多
|
||||
</text>
|
||||
<text
|
||||
v-else-if="finished"
|
||||
class="load-more-footer__finished"
|
||||
>
|
||||
没有更多了
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.load-more-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32rpx 0 16rpx;
|
||||
}
|
||||
|
||||
.load-more-footer__loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.load-more-footer__spinner {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
border: 4rpx solid #d6e4ff;
|
||||
border-top-color: $color-primary;
|
||||
border-radius: 50%;
|
||||
animation: load-more-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.load-more-footer__text,
|
||||
.load-more-footer__finished {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
@keyframes load-more-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,41 +1,319 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import AppTabBar from '@/components/AppTabBar.vue'
|
||||
import {
|
||||
CASE_CATEGORIES,
|
||||
CASE_LIBRARY_LIST,
|
||||
CASE_PAGE_SIZE,
|
||||
DEFAULT_CASE_FILTER,
|
||||
} from '@/constants/case-library'
|
||||
import type { CaseCategory, CaseFilterValues } from '@/types/case-library'
|
||||
import { cloneCaseFilter, matchCaseFilter } from '@/utils/case-filter'
|
||||
import CaseCard from './components/CaseCard.vue'
|
||||
import CaseFilterPanel from './components/CaseFilterPanel.vue'
|
||||
import LoadMoreFooter from './components/LoadMoreFooter.vue'
|
||||
|
||||
const LOAD_MORE_DELAY = 600
|
||||
|
||||
const keyword = ref('')
|
||||
const activeCategory = ref<CaseCategory>('全部')
|
||||
const caseList = ref(CASE_LIBRARY_LIST.map(item => ({ ...item })))
|
||||
const pageSize = ref(CASE_PAGE_SIZE)
|
||||
const loadingMore = ref(false)
|
||||
const filterVisible = ref(false)
|
||||
const appliedFilter = ref<CaseFilterValues>(cloneCaseFilter(DEFAULT_CASE_FILTER))
|
||||
|
||||
const filteredCases = computed(() => {
|
||||
const text = keyword.value.trim()
|
||||
|
||||
return caseList.value.filter((item) => {
|
||||
const matchCategory = activeCategory.value === '全部' || item.category === activeCategory.value
|
||||
const matchKeyword = !text || item.title.includes(text) || item.industry.includes(text) || item.category.includes(text)
|
||||
const matchFilter = matchCaseFilter(item, appliedFilter.value)
|
||||
return matchCategory && matchKeyword && matchFilter
|
||||
})
|
||||
})
|
||||
|
||||
const visibleCases = computed(() => filteredCases.value.slice(0, pageSize.value))
|
||||
|
||||
const hasMore = computed(() => pageSize.value < filteredCases.value.length)
|
||||
|
||||
const loadFinished = computed(() => !hasMore.value && visibleCases.value.length > 0)
|
||||
|
||||
watch([keyword, activeCategory, appliedFilter], () => {
|
||||
pageSize.value = CASE_PAGE_SIZE
|
||||
})
|
||||
|
||||
function handleSelectCategory(category: CaseCategory) {
|
||||
activeCategory.value = category
|
||||
}
|
||||
|
||||
function handleToggleFavorite(id: string) {
|
||||
caseList.value = caseList.value.map(item =>
|
||||
item.id === id ? { ...item, favorite: !item.favorite } : item,
|
||||
)
|
||||
}
|
||||
|
||||
function handleLoadMore() {
|
||||
if (loadingMore.value || !hasMore.value)
|
||||
return
|
||||
|
||||
loadingMore.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
pageSize.value += CASE_PAGE_SIZE
|
||||
loadingMore.value = false
|
||||
}, LOAD_MORE_DELAY)
|
||||
}
|
||||
|
||||
function handleOpenFilter() {
|
||||
filterVisible.value = true
|
||||
}
|
||||
|
||||
function handleCloseFilter() {
|
||||
filterVisible.value = false
|
||||
}
|
||||
|
||||
function handleConfirmFilter(value: CaseFilterValues) {
|
||||
appliedFilter.value = cloneCaseFilter(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="placeholder-page">
|
||||
<text class="placeholder-page__title">
|
||||
案例库
|
||||
</text>
|
||||
<text class="placeholder-page__desc">
|
||||
功能开发中
|
||||
</text>
|
||||
<view class="case-library">
|
||||
<view class="case-library__header">
|
||||
<text class="case-library__title">
|
||||
案例库
|
||||
</text>
|
||||
|
||||
<view class="case-library__search-row">
|
||||
<view class="case-library__search">
|
||||
<image
|
||||
class="case-library__search-icon"
|
||||
src="/static/icons/icon-search.svg"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<input
|
||||
v-model="keyword"
|
||||
class="case-library__search-input"
|
||||
placeholder="搜索案例标题、描述、标签..."
|
||||
placeholder-class="case-library__search-placeholder"
|
||||
>
|
||||
</view>
|
||||
<view class="case-library__favorite-btn">
|
||||
<image
|
||||
class="case-library__favorite-btn-icon"
|
||||
src="/static/icons/icon-heart.svg"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="case-library__tabs"
|
||||
scroll-x
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view class="case-library__tabs-inner">
|
||||
<text
|
||||
v-for="category in CASE_CATEGORIES"
|
||||
:key="category"
|
||||
class="case-library__tab"
|
||||
:class="{ 'case-library__tab--active': activeCategory === category }"
|
||||
@tap="handleSelectCategory(category)"
|
||||
>
|
||||
{{ category }}
|
||||
</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="case-library__toolbar">
|
||||
<text class="case-library__count">
|
||||
共 {{ filteredCases.length }}
|
||||
</text>
|
||||
<view
|
||||
class="case-library__filter"
|
||||
@tap="handleOpenFilter"
|
||||
>
|
||||
<image
|
||||
class="case-library__filter-icon"
|
||||
src="/static/icons/icon-filter.svg"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="case-library__filter-text">
|
||||
筛选
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="case-library__scroll"
|
||||
scroll-y
|
||||
:show-scrollbar="false"
|
||||
:lower-threshold="120"
|
||||
@scrolltolower="handleLoadMore"
|
||||
>
|
||||
<view class="case-library__list">
|
||||
<CaseCard
|
||||
v-for="item in visibleCases"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@toggle-favorite="handleToggleFavorite"
|
||||
/>
|
||||
|
||||
<LoadMoreFooter
|
||||
:loading="loadingMore"
|
||||
:has-more="hasMore"
|
||||
:finished="loadFinished"
|
||||
@load-more="handleLoadMore"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<AppTabBar active="case" />
|
||||
|
||||
<CaseFilterPanel
|
||||
:visible="filterVisible"
|
||||
:cases="caseList"
|
||||
:value="appliedFilter"
|
||||
@close="handleCloseFilter"
|
||||
@confirm="handleConfirmFilter"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.placeholder-page {
|
||||
.case-library {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: $color-bg-section;
|
||||
}
|
||||
|
||||
.case-library__header {
|
||||
flex-shrink: 0;
|
||||
padding: calc(var(--status-bar-height, 44px) + 12rpx) 24rpx 0;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.placeholder-page__title {
|
||||
.case-library__title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-primary;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.placeholder-page__desc {
|
||||
margin-top: 16rpx;
|
||||
.case-library__search-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.case-library__search {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
background: $color-bg-section;
|
||||
border-radius: 36rpx;
|
||||
}
|
||||
|
||||
.case-library__search-icon {
|
||||
flex-shrink: 0;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.case-library__search-input {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.case-library__search-placeholder {
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
|
||||
.case-library__favorite-btn {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.case-library__favorite-btn-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.case-library__tabs {
|
||||
margin-top: 24rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.case-library__tabs-inner {
|
||||
display: inline-flex;
|
||||
gap: 32rpx;
|
||||
padding-right: 24rpx;
|
||||
}
|
||||
|
||||
.case-library__tab {
|
||||
flex-shrink: 0;
|
||||
padding-bottom: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-secondary;
|
||||
|
||||
&--active {
|
||||
font-weight: 600;
|
||||
color: $color-text-primary;
|
||||
border-bottom: 4rpx solid $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.case-library__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 0 24rpx;
|
||||
}
|
||||
|
||||
.case-library__count {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.case-library__filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.case-library__filter-icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.case-library__filter-text {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.case-library__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.case-library__list {
|
||||
padding: 0 24rpx calc(140rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
</style>
|
||||
|
||||
335
src/pages/home-index/business-detail/index.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getBusinessDetail } from '@/constants/business-detail'
|
||||
import type { BusinessDetailContent } from '@/types/business-detail'
|
||||
import { navigateBackWithFallback } from '@/utils/navigate-back'
|
||||
import heroBg from '@/static/icons/background.png'
|
||||
|
||||
const detail = ref<BusinessDetailContent | null>(null)
|
||||
|
||||
onLoad((options) => {
|
||||
const id = (options?.id as string) || 'software'
|
||||
detail.value = getBusinessDetail(id) || null
|
||||
|
||||
if (!detail.value) {
|
||||
uni.showToast({ title: '业务详情不存在', icon: 'none' })
|
||||
setTimeout(() => uni.navigateBack(), 1500)
|
||||
}
|
||||
})
|
||||
|
||||
function handleBack() {
|
||||
navigateBackWithFallback('/pages/home-index/index')
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!detail.value)
|
||||
return
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/home-index/requirement-submit/index?businessType=${encodeURIComponent(detail.value.title)}&detailId=${detail.value.id}`,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
v-if="detail"
|
||||
class="business-detail"
|
||||
>
|
||||
<view class="business-detail__navbar">
|
||||
<view
|
||||
class="business-detail__back"
|
||||
@tap="handleBack"
|
||||
>
|
||||
<image
|
||||
class="business-detail__back-icon"
|
||||
src="/static/icons/icon-back.svg"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<text class="business-detail__navbar-title">
|
||||
业务详情
|
||||
</text>
|
||||
<view class="business-detail__navbar-placeholder" />
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="business-detail__scroll"
|
||||
scroll-y
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view class="business-detail__hero">
|
||||
<image
|
||||
class="business-detail__hero-bg"
|
||||
:src="heroBg"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="business-detail__hero-inner">
|
||||
<image
|
||||
class="business-detail__hero-icon"
|
||||
:src="detail.icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="business-detail__hero-title">
|
||||
{{ detail.title }}
|
||||
</text>
|
||||
<text class="business-detail__hero-subtitle">
|
||||
{{ detail.subtitle }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="business-detail__section">
|
||||
<text class="business-detail__section-title">
|
||||
服务类型
|
||||
</text>
|
||||
<view class="business-detail__type-row">
|
||||
<view
|
||||
v-for="type in detail.serviceTypes"
|
||||
:key="type.id"
|
||||
class="business-detail__type-item"
|
||||
>
|
||||
<image
|
||||
class="business-detail__type-icon"
|
||||
:src="type.icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="business-detail__type-label">
|
||||
{{ type.label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="business-detail__section">
|
||||
<text class="business-detail__section-title">
|
||||
{{ detail.contentTitle }}
|
||||
</text>
|
||||
|
||||
<view class="business-detail__video">
|
||||
<view class="business-detail__video-play">
|
||||
<text class="business-detail__video-play-icon">
|
||||
▶
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="business-detail__content">
|
||||
<text
|
||||
v-for="(paragraph, index) in detail.contentSections"
|
||||
:key="index"
|
||||
class="business-detail__paragraph"
|
||||
:class="{ 'business-detail__paragraph--title': paragraph.startsWith('一、') || paragraph.startsWith('二、') }"
|
||||
>
|
||||
{{ paragraph }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="business-detail__footer">
|
||||
<view
|
||||
class="business-detail__submit-btn"
|
||||
@tap="handleSubmit"
|
||||
>
|
||||
立即提报
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.business-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: $color-bg-section;
|
||||
}
|
||||
|
||||
.business-detail__navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
padding: calc(var(--status-bar-height, 44px) + 12rpx) 24rpx 12rpx;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.business-detail__back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.business-detail__back-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.business-detail__navbar-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.business-detail__navbar-placeholder {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.business-detail__scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.business-detail__hero {
|
||||
position: relative;
|
||||
margin: 20rpx 24rpx 0;
|
||||
overflow: hidden;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.business-detail__hero-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.business-detail__hero-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 24rpx 24rpx 28rpx;
|
||||
}
|
||||
|
||||
.business-detail__hero-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
}
|
||||
|
||||
.business-detail__hero-title {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.business-detail__hero-subtitle {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.business-detail__section {
|
||||
margin: 20rpx 24rpx 0;
|
||||
padding: 28rpx 24rpx 32rpx;
|
||||
background: $color-bg-page;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.business-detail__section-title {
|
||||
display: block;
|
||||
margin-bottom: 28rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.business-detail__type-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.business-detail__type-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.business-detail__type-icon {
|
||||
width: 91rpx;
|
||||
height: 91rpx;
|
||||
}
|
||||
|
||||
.business-detail__type-label {
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: $color-text-primary;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.business-detail__video {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 360rpx;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.business-detail__video-play {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.business-detail__video-play-icon {
|
||||
margin-left: 6rpx;
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.business-detail__content {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.business-detail__paragraph {
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.8;
|
||||
color: $color-text-primary;
|
||||
|
||||
&--title {
|
||||
margin-top: 8rpx;
|
||||
margin-bottom: 12rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.business-detail__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom));
|
||||
background: $color-bg-page;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.business-detail__submit-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 88rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background: $color-primary;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
</style>
|
||||
112
src/pages/home-index/components/HomeFloatingActions.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
function handleQuickSubmit() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/home-index/requirement-submit/index',
|
||||
})
|
||||
}
|
||||
|
||||
function handleCustomerService() {
|
||||
uni.showToast({
|
||||
title: '客服中心开发中',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="home-floating-actions">
|
||||
<view
|
||||
class="home-floating-actions__item"
|
||||
@tap="handleQuickSubmit"
|
||||
>
|
||||
<view class="home-floating-actions__icon home-floating-actions__icon--submit">
|
||||
<text class="home-floating-actions__plus">
|
||||
+
|
||||
</text>
|
||||
</view>
|
||||
<text class="home-floating-actions__label">
|
||||
快捷提报
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="home-floating-actions__item"
|
||||
@tap="handleCustomerService"
|
||||
>
|
||||
<view class="home-floating-actions__icon home-floating-actions__icon--service">
|
||||
<image
|
||||
class="home-floating-actions__service-icon"
|
||||
src="/static/icons/icon-float-service.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<text class="home-floating-actions__label">
|
||||
客服中心
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.home-floating-actions {
|
||||
position: fixed;
|
||||
right: 24rpx;
|
||||
bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||
z-index: 120;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.home-floating-actions__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 72rpx;
|
||||
padding: 0 28rpx 0 12rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 999rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.home-floating-actions__icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
|
||||
&--submit {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
&--service {
|
||||
background: #e8f0ff;
|
||||
}
|
||||
}
|
||||
|
||||
.home-floating-actions__plus {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.home-floating-actions__service-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
.home-floating-actions__label {
|
||||
margin-left: 16rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: $color-text-primary;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ProjectItem } from '@/types/home'
|
||||
import cardBg from '@/static/icons/new-card.png'
|
||||
|
||||
defineProps<{
|
||||
item: ProjectItem
|
||||
@@ -8,6 +9,12 @@ defineProps<{
|
||||
|
||||
<template>
|
||||
<view class="project-card">
|
||||
<image
|
||||
class="project-card__bg"
|
||||
:src="cardBg"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="project-card__inner">
|
||||
<view class="project-card__header">
|
||||
<text class="project-card__title">
|
||||
{{ item.title }}
|
||||
@@ -24,18 +31,14 @@ defineProps<{
|
||||
>
|
||||
{{ item.category }}
|
||||
</text>
|
||||
<text class="project-card__no">
|
||||
项目编号:{{ item.projectNo }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="project-card__progress-row">
|
||||
<text class="project-card__progress-label">
|
||||
进行中
|
||||
</text>
|
||||
<text class="project-card__progress-value">
|
||||
{{ item.progress }}%
|
||||
</text>
|
||||
<view class="project-card__meta-info">
|
||||
<text class="project-card__no">
|
||||
项目编号:{{ item.projectNo }}
|
||||
</text>
|
||||
<text class="project-card__progress-value">
|
||||
{{ item.progress }}%
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="project-card__progress-bar">
|
||||
@@ -53,6 +56,7 @@ defineProps<{
|
||||
查看详情 >
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -60,11 +64,25 @@ defineProps<{
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.project-card {
|
||||
padding: 28rpx;
|
||||
position: relative;
|
||||
margin-top: 20rpx;
|
||||
background: $color-bg-page;
|
||||
overflow: hidden;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.project-card__bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.project-card__inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.project-card__header {
|
||||
@@ -93,45 +111,52 @@ defineProps<{
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.project-card__meta-info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.project-card__tag {
|
||||
flex-shrink: 0;
|
||||
padding: 4rpx 12rpx;
|
||||
font-size: 20rpx;
|
||||
border-radius: 6rpx;
|
||||
font-weight: 500;
|
||||
width: 100rpx;
|
||||
text-align: center;
|
||||
height: 20rpx;
|
||||
line-height: 20rpx;
|
||||
|
||||
&--orange {
|
||||
color: $color-tag-orange;
|
||||
background: $color-tag-orange-bg;
|
||||
}
|
||||
|
||||
&--peach {
|
||||
color: #e07b4a;
|
||||
background: #fff0e8;
|
||||
color: #875045;
|
||||
background: #FFF1EA;
|
||||
}
|
||||
}
|
||||
|
||||
.project-card__no {
|
||||
margin-left: 12rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 22rpx;
|
||||
color: $color-text-secondary;
|
||||
color: #666666;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.project-card__progress-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.project-card__progress-label,
|
||||
.project-card__progress-value {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-secondary;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.project-card__progress-bar {
|
||||
height: 12rpx;
|
||||
margin-top: 12rpx;
|
||||
margin-top: 24rpx;
|
||||
overflow: hidden;
|
||||
background: #eef2ff;
|
||||
border-radius: 6rpx;
|
||||
@@ -152,11 +177,11 @@ defineProps<{
|
||||
|
||||
.project-card__time {
|
||||
font-size: 22rpx;
|
||||
color: $color-text-secondary;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.project-card__detail {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-link;
|
||||
color: #666666;
|
||||
}
|
||||
</style>
|
||||
|
||||
244
src/pages/home-index/components/QuickEntryEditor.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<script setup lang="ts">
|
||||
import type { QuickEntryItem } from '@/types/home'
|
||||
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
pinnedEntries: QuickEntryItem[]
|
||||
otherEntries: QuickEntryItem[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
remove: [id: string]
|
||||
add: [id: string]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
v-if="visible"
|
||||
class="quick-entry-editor"
|
||||
>
|
||||
<view
|
||||
class="quick-entry-editor__mask"
|
||||
@tap="emit('close')"
|
||||
/>
|
||||
|
||||
<view
|
||||
class="quick-entry-editor__panel"
|
||||
@tap.stop
|
||||
>
|
||||
<view class="quick-entry-editor__header">
|
||||
<text class="quick-entry-editor__title">
|
||||
快捷入口
|
||||
</text>
|
||||
<text
|
||||
class="quick-entry-editor__close"
|
||||
@tap="emit('close')"
|
||||
>
|
||||
×
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="quick-entry-editor__body"
|
||||
scroll-y
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view class="quick-entry-editor__grid quick-entry-editor__grid--pinned">
|
||||
<view
|
||||
v-for="item in pinnedEntries"
|
||||
:key="item.id"
|
||||
class="quick-entry-editor__item"
|
||||
>
|
||||
<view class="quick-entry-editor__icon-wrap">
|
||||
<image
|
||||
class="quick-entry-editor__icon"
|
||||
:src="item.icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view
|
||||
class="quick-entry-editor__badge"
|
||||
@tap.stop="emit('remove', item.id)"
|
||||
>
|
||||
<img src="@/static/icons/delete.png" alt="close">
|
||||
</view>
|
||||
</view>
|
||||
<text class="quick-entry-editor__label">
|
||||
{{ item.label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="quick-entry-editor__section-title">
|
||||
其他入口
|
||||
</text>
|
||||
|
||||
<view class="quick-entry-editor__grid quick-entry-editor__grid--other">
|
||||
<view
|
||||
v-for="item in otherEntries"
|
||||
:key="item.id"
|
||||
class="quick-entry-editor__item"
|
||||
>
|
||||
<view class="quick-entry-editor__icon-wrap">
|
||||
<image
|
||||
class="quick-entry-editor__icon"
|
||||
:src="item.icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view
|
||||
class="quick-entry-editor__badge"
|
||||
@tap.stop="emit('add', item.id)"
|
||||
>
|
||||
<img src="@/static/icons/add.png" alt="close">
|
||||
</view>
|
||||
</view>
|
||||
<text class="quick-entry-editor__label">
|
||||
{{ item.label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.quick-entry-editor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.quick-entry-editor__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.quick-entry-editor__panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
background: $color-bg-page;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.quick-entry-editor__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
}
|
||||
|
||||
.quick-entry-editor__title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.quick-entry-editor__close {
|
||||
width: 48rpx;
|
||||
font-size: 40rpx;
|
||||
line-height: 1;
|
||||
color: $color-text-secondary;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.quick-entry-editor__body {
|
||||
flex: 1;
|
||||
max-height: calc(80vh - 100rpx);
|
||||
padding: 0 24rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.quick-entry-editor__section-title {
|
||||
display: block;
|
||||
margin: 32rpx 8rpx 24rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.quick-entry-editor__grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&--pinned {
|
||||
justify-content: space-between;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
&--other {
|
||||
row-gap: 28rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-entry-editor__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.quick-entry-editor__icon-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.quick-entry-editor__icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
.quick-entry-editor__badge {
|
||||
position: absolute;
|
||||
top: -4rpx;
|
||||
right: -4rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
background: #c8cdd4;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.quick-entry-editor__badge-text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.quick-entry-editor__label {
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: $color-text-primary;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { ServiceItem } from '@/types/home'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
item: ServiceItem
|
||||
}>()
|
||||
|
||||
function handleTap() {
|
||||
if (!props.item.detailId)
|
||||
return
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/home-index/business-detail/index?id=${props.item.detailId}`,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -12,7 +21,9 @@ defineProps<{
|
||||
:class="[
|
||||
`service-card--${item.theme}`,
|
||||
{ 'service-card--with-illus': item.illus },
|
||||
{ 'service-card--clickable': item.detailId },
|
||||
]"
|
||||
@tap="handleTap"
|
||||
>
|
||||
<image
|
||||
v-if="item.illus"
|
||||
@@ -63,6 +74,10 @@ defineProps<{
|
||||
&--orange {
|
||||
background: linear-gradient( 180deg, #F7D3C6 0%, #FFFFFF 100%);
|
||||
}
|
||||
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.service-card__illus {
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import AppTabBar from '@/components/AppTabBar.vue'
|
||||
import { useGreeting } from '@/hooks/use-greeting'
|
||||
import type { ProjectItem, QuickEntryItem, ServiceItem } from '@/types/home'
|
||||
import { useQuickEntries } from '@/hooks/use-quick-entries'
|
||||
import type { ProjectItem, ServiceItem } from '@/types/home'
|
||||
import headerBg from '@/static/icons/1.png'
|
||||
import softwareIllus from '@/static/icons/software.png'
|
||||
import hardwareIllus from '@/static/icons/hardware.png'
|
||||
import iotIllus from '@/static/icons/iot.png'
|
||||
import productionIllus from '@/static/icons/production.png'
|
||||
import HomeFloatingActions from './components/HomeFloatingActions.vue'
|
||||
import ProjectCard from './components/ProjectCard.vue'
|
||||
import QuickEntryEditor from './components/QuickEntryEditor.vue'
|
||||
import ServiceCard from './components/ServiceCard.vue'
|
||||
|
||||
const greeting = useGreeting()
|
||||
const {
|
||||
pinnedEntries,
|
||||
otherEntries,
|
||||
editorVisible,
|
||||
openEditor,
|
||||
closeEditor,
|
||||
removeFromPinned,
|
||||
addToPinned,
|
||||
} = useQuickEntries()
|
||||
|
||||
const services: ServiceItem[] = [
|
||||
{
|
||||
@@ -19,6 +31,7 @@ const services: ServiceItem[] = [
|
||||
desc: 'Web/小程序/APP/二次开发/桌面工具等',
|
||||
theme: 'blue',
|
||||
illus: softwareIllus,
|
||||
detailId: 'software',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
@@ -43,14 +56,6 @@ const services: ServiceItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const quickEntries: QuickEntryItem[] = [
|
||||
{ id: '1', label: '项目中心', icon: '/static/icons/quick-project.png', bgColor: '#E8F0FF' },
|
||||
{ id: '2', label: '快捷提报', icon: '/static/icons/quick-report.png', bgColor: '#E8FAF4' },
|
||||
{ id: '3', label: '在线客服', icon: '/static/icons/quick-service.png', bgColor: '#F3E8FF' },
|
||||
{ id: '4', label: '合同管理', icon: '/static/icons/quick-contract.png', bgColor: '#FEE2E2' },
|
||||
{ id: '5', label: '财务管理', icon: '/static/icons/quick-finance.png', bgColor: '#E0F2FE' },
|
||||
]
|
||||
|
||||
const projects: ProjectItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
@@ -70,7 +75,7 @@ const projects: ProjectItem[] = [
|
||||
progress: 65,
|
||||
status: '进行中',
|
||||
updateTime: '2026-8-13 11:04:12',
|
||||
tagTheme: 'peach',
|
||||
tagTheme: 'orange',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
@@ -146,13 +151,16 @@ const projects: ProjectItem[] = [
|
||||
<text class="home__section-title">
|
||||
快捷入口
|
||||
</text>
|
||||
<text class="home__section-action">
|
||||
<text
|
||||
class="home__section-action"
|
||||
@tap="openEditor"
|
||||
>
|
||||
编辑
|
||||
</text>
|
||||
</view>
|
||||
<view class="home__quick-row">
|
||||
<view
|
||||
v-for="entry in quickEntries"
|
||||
v-for="entry in pinnedEntries"
|
||||
:key="entry.id"
|
||||
class="home__quick-item"
|
||||
>
|
||||
@@ -168,7 +176,7 @@ const projects: ProjectItem[] = [
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="home__section home__section--last">
|
||||
<view class="home__sectionss home__section--last">
|
||||
<view class="home__section-header">
|
||||
<text class="home__section-title">
|
||||
在办项目
|
||||
@@ -180,17 +188,27 @@ const projects: ProjectItem[] = [
|
||||
|
||||
<view class="home__stats-row">
|
||||
<view class="home__stat-card home__stat-card--blue">
|
||||
<text class="home__stat-value">
|
||||
3 个项目
|
||||
</text>
|
||||
<view class="home__stat-value">
|
||||
<text class="home__stat-num">
|
||||
3
|
||||
</text>
|
||||
<text class="home__stat-unit">
|
||||
个项目
|
||||
</text>
|
||||
</view>
|
||||
<text class="home__stat-label">
|
||||
进行中
|
||||
</text>
|
||||
</view>
|
||||
<view class="home__stat-card home__stat-card--orange">
|
||||
<text class="home__stat-value">
|
||||
3 项待办
|
||||
</text>
|
||||
<view class="home__stat-value">
|
||||
<text class="home__stat-num">
|
||||
3
|
||||
</text>
|
||||
<text class="home__stat-unit">
|
||||
项待办
|
||||
</text>
|
||||
</view>
|
||||
<text class="home__stat-label">
|
||||
待您处理
|
||||
</text>
|
||||
@@ -211,6 +229,17 @@ const projects: ProjectItem[] = [
|
||||
</scroll-view>
|
||||
|
||||
<AppTabBar active="home" />
|
||||
|
||||
<HomeFloatingActions />
|
||||
|
||||
<QuickEntryEditor
|
||||
:visible="editorVisible"
|
||||
:pinned-entries="pinnedEntries"
|
||||
:other-entries="otherEntries"
|
||||
@close="closeEditor"
|
||||
@remove="removeFromPinned"
|
||||
@add="addToPinned"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -361,6 +390,14 @@ const projects: ProjectItem[] = [
|
||||
padding-top: 20rpx
|
||||
}
|
||||
|
||||
.home__sectionss {
|
||||
margin-bottom: 20rpx;
|
||||
width: 711.83rpx;
|
||||
background: $color-bg-page;
|
||||
border-radius: 20rpx;
|
||||
padding-top: 20rpx
|
||||
}
|
||||
|
||||
.home__section--last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -422,34 +459,52 @@ const projects: ProjectItem[] = [
|
||||
.home__stats-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.home__stat-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 28rpx 24rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
text-align: center;
|
||||
|
||||
&--blue {
|
||||
background: $color-primary-light;
|
||||
background: linear-gradient(180deg, #D3E1FC 0%, #FFFFFF 100%);
|
||||
}
|
||||
|
||||
&--orange {
|
||||
background: $color-stat-orange-bg;
|
||||
background: linear-gradient(180deg, #FFECE1 0%, #FFFFFF 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.home__stat-value {
|
||||
font-size: 32rpx;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.home__stat-num {
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.home__stat-unit {
|
||||
margin-left: 4rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.home__stat-label {
|
||||
margin-top: 8rpx;
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-secondary;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.home__recent-title {
|
||||
|
||||
706
src/pages/home-index/requirement-submit/index.vue
Normal file
@@ -0,0 +1,706 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { BUSINESS_TYPE_DETAIL_ID, BUSINESS_TYPE_OPTIONS } from '@/constants/business-types'
|
||||
import { navigateBackWithFallback } from '@/utils/navigate-back'
|
||||
import { navigateToResultPage } from '@/utils/navigate-result'
|
||||
import deleteIcon from '@/static/icons/X.png'
|
||||
import pdfIcon from '@/static/icons/PDF.png'
|
||||
import pngIcon from '@/static/icons/PNG.png'
|
||||
|
||||
const businessType = ref('')
|
||||
const detailId = ref('software')
|
||||
const businessTypeDropdownVisible = ref(false)
|
||||
const businessTypeOptions = BUSINESS_TYPE_OPTIONS
|
||||
const projectName = ref('')
|
||||
const description = ref('')
|
||||
const budget = ref('')
|
||||
const cycle = ref('')
|
||||
const userName = ref('王永志')
|
||||
const userPhone = ref('186****9618')
|
||||
const remark = ref('')
|
||||
|
||||
const budgetOptions = ['1万以下', '1-5万', '5-10万', '10万+']
|
||||
|
||||
const attachments = ref([
|
||||
{ id: '1', name: '需求文档.pdf', type: 'pdf', icon: pdfIcon },
|
||||
{ id: '2', name: '参考图.png', type: 'png', icon: pdfIcon},
|
||||
{ id: '3', name: '合同模板.pdf', type: 'pdf', icon: pngIcon },
|
||||
])
|
||||
|
||||
const descLength = computed(() => description.value.length)
|
||||
|
||||
onLoad((options) => {
|
||||
if (options?.detailId)
|
||||
detailId.value = options.detailId as string
|
||||
|
||||
if (options?.businessType) {
|
||||
businessType.value = decodeURIComponent(options.businessType as string)
|
||||
if (!options.detailId)
|
||||
detailId.value = BUSINESS_TYPE_DETAIL_ID[businessType.value] || 'software'
|
||||
}
|
||||
})
|
||||
|
||||
function handleBack() {
|
||||
closeBusinessTypeDropdown()
|
||||
navigateBackWithFallback(`/pages/home-index/business-detail/index?id=${detailId.value}`)
|
||||
}
|
||||
|
||||
function toggleBusinessTypeDropdown() {
|
||||
businessTypeDropdownVisible.value = !businessTypeDropdownVisible.value
|
||||
}
|
||||
|
||||
function closeBusinessTypeDropdown() {
|
||||
businessTypeDropdownVisible.value = false
|
||||
}
|
||||
|
||||
function handleSelectBusinessType(option: string) {
|
||||
businessType.value = option
|
||||
closeBusinessTypeDropdown()
|
||||
}
|
||||
|
||||
function handleSelectBudget(value: string) {
|
||||
budget.value = value
|
||||
}
|
||||
|
||||
function handleRemoveAttachment(id: string) {
|
||||
attachments.value = attachments.value.filter(item => item.id !== id)
|
||||
}
|
||||
|
||||
function handleAddAttachment() {
|
||||
uni.showToast({ title: '附件上传开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleSaveDraft() {
|
||||
uni.showToast({ title: '草稿已保存', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
projectName.value = ''
|
||||
description.value = ''
|
||||
budget.value = ''
|
||||
cycle.value = ''
|
||||
remark.value = ''
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!businessType.value) {
|
||||
uni.showToast({ title: '请选择业务类型', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!projectName.value.trim()) {
|
||||
uni.showToast({ title: '项目名称不能为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!description.value.trim()) {
|
||||
uni.showToast({ title: '请填写核心需求描述', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
navigateToResultPage('submitSuccess')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="requirement-submit">
|
||||
<view class="requirement-submit__navbar">
|
||||
<view
|
||||
class="requirement-submit__back"
|
||||
@tap="handleBack"
|
||||
>
|
||||
<image
|
||||
class="requirement-submit__back-icon"
|
||||
src="/static/icons/icon-back.svg"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<text class="requirement-submit__navbar-title">
|
||||
需求提报
|
||||
</text>
|
||||
<view class="requirement-submit__navbar-placeholder" />
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="requirement-submit__scroll"
|
||||
scroll-y
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view class="requirement-submit__card requirement-submit__card--dropdown">
|
||||
<view class="requirement-submit__field">
|
||||
<text class="requirement-submit__label">
|
||||
业务类型<text class="requirement-submit__required">
|
||||
*
|
||||
</text>
|
||||
</text>
|
||||
<view class="requirement-submit__select-wrap">
|
||||
<view
|
||||
class="requirement-submit__select"
|
||||
@tap.stop="toggleBusinessTypeDropdown"
|
||||
>
|
||||
<text
|
||||
class="requirement-submit__select-text"
|
||||
:class="{ 'requirement-submit__select-text--placeholder': !businessType }"
|
||||
>
|
||||
{{ businessType || '选择下方推荐的服务或手动输入服务名称' }}
|
||||
</text>
|
||||
<text
|
||||
class="requirement-submit__select-arrow"
|
||||
:class="{ 'requirement-submit__select-arrow--open': businessTypeDropdownVisible }"
|
||||
>
|
||||
▾
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="businessTypeDropdownVisible"
|
||||
class="requirement-submit__dropdown"
|
||||
@tap.stop
|
||||
>
|
||||
<view
|
||||
v-for="option in businessTypeOptions"
|
||||
:key="option"
|
||||
class="requirement-submit__dropdown-item"
|
||||
:class="{ 'requirement-submit__dropdown-item--active': businessType === option }"
|
||||
@tap="handleSelectBusinessType(option)"
|
||||
>
|
||||
{{ option }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="requirement-submit__card">
|
||||
<view class="requirement-submit__field">
|
||||
<text class="requirement-submit__label">
|
||||
项目名称<text class="requirement-submit__required">
|
||||
*
|
||||
</text>
|
||||
</text>
|
||||
<input
|
||||
v-model="projectName"
|
||||
class="requirement-submit__input requirement-submit__input--box"
|
||||
placeholder="项目名称不能为空"
|
||||
placeholder-class="requirement-submit__placeholder"
|
||||
>
|
||||
</view>
|
||||
|
||||
<view class="requirement-submit__field">
|
||||
<text class="requirement-submit__label">
|
||||
核心需求描述<text class="requirement-submit__required">
|
||||
*
|
||||
</text>
|
||||
</text>
|
||||
<view class="requirement-submit__textarea-wrap">
|
||||
<textarea
|
||||
v-model="description"
|
||||
class="requirement-submit__textarea"
|
||||
placeholder="请详细描述您的核心需求,包括功能目标、使用场景、期望效果等"
|
||||
placeholder-class="requirement-submit__placeholder"
|
||||
maxlength="500"
|
||||
/>
|
||||
<text class="requirement-submit__counter">
|
||||
{{ descLength }}/500字
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="requirement-submit__field">
|
||||
<text class="requirement-submit__label">
|
||||
预算范围
|
||||
</text>
|
||||
<view class="requirement-submit__budget-row">
|
||||
<view
|
||||
v-for="option in budgetOptions"
|
||||
:key="option"
|
||||
class="requirement-submit__budget-item"
|
||||
:class="{ 'requirement-submit__budget-item--active': budget === option }"
|
||||
@tap="handleSelectBudget(option)"
|
||||
>
|
||||
{{ option }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="requirement-submit__field">
|
||||
<text class="requirement-submit__label">
|
||||
预计开发周期
|
||||
</text>
|
||||
<view class="requirement-submit__cycle-row">
|
||||
<input
|
||||
v-model="cycle"
|
||||
class="requirement-submit__input requirement-submit__input--box requirement-submit__input--cycle"
|
||||
placeholder="请输入预计开发周期"
|
||||
placeholder-class="requirement-submit__placeholder"
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="requirement-submit__field">
|
||||
<text class="requirement-submit__label">
|
||||
用户名称
|
||||
</text>
|
||||
<input
|
||||
v-model="userName"
|
||||
class="requirement-submit__input requirement-submit__input--box"
|
||||
placeholder="请输入用户名称"
|
||||
placeholder-class="requirement-submit__placeholder"
|
||||
>
|
||||
</view>
|
||||
|
||||
<view class="requirement-submit__field">
|
||||
<text class="requirement-submit__label">
|
||||
用户手机号
|
||||
</text>
|
||||
<input
|
||||
v-model="userPhone"
|
||||
class="requirement-submit__input requirement-submit__input--box"
|
||||
placeholder="请输入用户手机号"
|
||||
placeholder-class="requirement-submit__placeholder"
|
||||
>
|
||||
</view>
|
||||
|
||||
<view class="requirement-submit__field">
|
||||
<text class="requirement-submit__label">
|
||||
附件上传
|
||||
</text>
|
||||
<text class="requirement-submit__hint">
|
||||
支持图片/文档/压缩包
|
||||
</text>
|
||||
<view class="requirement-submit__attachment-row">
|
||||
<view
|
||||
v-for="file in attachments"
|
||||
:key="file.id"
|
||||
class="requirement-submit__attachment-item"
|
||||
>
|
||||
<view class="requirement-submit__attachment-thumb">
|
||||
<image
|
||||
class="requirement-submit__attachment-icon"
|
||||
:src="file.icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="requirement-submit__attachment-remove"
|
||||
@tap="handleRemoveAttachment(file.id)"
|
||||
>
|
||||
<image
|
||||
class="requirement-submit__attachment-remove-icon"
|
||||
:src="deleteIcon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="requirement-submit__attachment-add"
|
||||
@tap="handleAddAttachment"
|
||||
>
|
||||
<text class="requirement-submit__attachment-add-icon">
|
||||
+
|
||||
</text>
|
||||
<text class="requirement-submit__attachment-add-text">
|
||||
添加附件
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="requirement-submit__card">
|
||||
<view class="requirement-submit__field">
|
||||
<text class="requirement-submit__label">
|
||||
补充备注
|
||||
</text>
|
||||
<textarea
|
||||
v-model="remark"
|
||||
class="requirement-submit__textarea requirement-submit__textarea--remark"
|
||||
placeholder="其他需要说明的事项..."
|
||||
placeholder-class="requirement-submit__placeholder"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view
|
||||
v-if="businessTypeDropdownVisible"
|
||||
class="requirement-submit__dropdown-mask"
|
||||
@tap="closeBusinessTypeDropdown"
|
||||
/>
|
||||
|
||||
<view class="requirement-submit__footer">
|
||||
<view
|
||||
class="requirement-submit__footer-btn requirement-submit__footer-btn--primary"
|
||||
@tap="handleSaveDraft"
|
||||
>
|
||||
保存草稿
|
||||
</view>
|
||||
<view
|
||||
class="requirement-submit__footer-btn requirement-submit__footer-btn--primary"
|
||||
@tap="handleReset"
|
||||
>
|
||||
重置
|
||||
</view>
|
||||
<view
|
||||
class="requirement-submit__footer-btn requirement-submit__footer-btn--primary"
|
||||
@tap="handleSubmit"
|
||||
>
|
||||
立即提报
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.requirement-submit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: $color-bg-section;
|
||||
}
|
||||
|
||||
.requirement-submit__navbar {
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
padding: calc(var(--status-bar-height, 44px) + 12rpx) 24rpx 12rpx;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.requirement-submit__back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__back-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__navbar-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.requirement-submit__navbar-placeholder {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 20rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.requirement-submit__card {
|
||||
padding: 28rpx 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: $color-bg-page;
|
||||
border-radius: 20rpx;
|
||||
|
||||
&--dropdown {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.requirement-submit__field {
|
||||
margin-bottom: 28rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.requirement-submit__label {
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.requirement-submit__required {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.requirement-submit__hint {
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.requirement-submit__input {
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-primary;
|
||||
|
||||
&--box {
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: $color-bg-page;
|
||||
border: 1rpx solid #e8eaef;
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&--cycle {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.requirement-submit__placeholder {
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
|
||||
.requirement-submit__select-wrap {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.requirement-submit__select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: $color-bg-page;
|
||||
border: 1rpx solid #e8eaef;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__select-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-primary;
|
||||
|
||||
&--placeholder {
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
.requirement-submit__select-arrow {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-secondary;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&--open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.requirement-submit__dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 8rpx);
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 21;
|
||||
overflow: hidden;
|
||||
background: $color-bg-page;
|
||||
border: 1rpx solid #e8eaef;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.requirement-submit__dropdown-item {
|
||||
padding: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-primary;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: $color-primary;
|
||||
background: $color-primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
.requirement-submit__dropdown-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.requirement-submit__textarea-wrap {
|
||||
position: relative;
|
||||
padding: 20rpx 24rpx 48rpx;
|
||||
background: $color-bg-page;
|
||||
border: 1rpx solid #e8eaef;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__textarea {
|
||||
width: 100%;
|
||||
min-height: 180rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
color: $color-text-primary;
|
||||
|
||||
&--remark {
|
||||
min-height: 140rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
background: $color-bg-page;
|
||||
border: 1rpx solid #e8eaef;
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.requirement-submit__counter {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
bottom: 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.requirement-submit__budget-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__budget-item {
|
||||
padding: 14rpx 28rpx;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-primary;
|
||||
background: $color-bg-page;
|
||||
border: 1rpx solid #e8eaef;
|
||||
border-radius: 12rpx;
|
||||
|
||||
&--active {
|
||||
color: $color-primary;
|
||||
background: $color-primary-light;
|
||||
border-color: $color-primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
.requirement-submit__cycle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.requirement-submit__cycle-unit {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.requirement-submit__attachment-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__attachment-item {
|
||||
position: relative;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__attachment-thumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f8f9fb;
|
||||
border: 1rpx solid #e8eaef;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__attachment-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__attachment-remove {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
right: -10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__attachment-remove-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__attachment-add {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: #f8f9fb;
|
||||
border: 1rpx dashed #d8dde6;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.requirement-submit__attachment-add-icon {
|
||||
font-size: 40rpx;
|
||||
line-height: 1;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.requirement-submit__attachment-add-text {
|
||||
margin-top: 8rpx;
|
||||
font-size: 20rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.requirement-submit__footer {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
|
||||
background: $color-bg-page;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.requirement-submit__footer-btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 80rpx;
|
||||
font-size: 28rpx;
|
||||
border-radius: 12rpx;
|
||||
|
||||
&--outline {
|
||||
color: $color-primary;
|
||||
background: $color-primary-light;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background: $color-primary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
BIN
src/static/icons/PDF.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/static/icons/PNG.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/static/icons/X.png
Normal file
|
After Width: | Height: | Size: 570 B |
BIN
src/static/icons/about.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/static/icons/add.png
Normal file
|
After Width: | Height: | Size: 979 B |
BIN
src/static/icons/address.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/static/icons/background.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/static/icons/contract-list.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/static/icons/delete.png
Normal file
|
After Width: | Height: | Size: 813 B |
BIN
src/static/icons/feedback.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/static/icons/help.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
3
src/static/icons/icon-filter.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 4H14M4 8H12M6 12H10" stroke="#2563EB" stroke-width="1.6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 199 B |
BIN
src/static/icons/icon-float-service.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
3
src/static/icons/icon-heart-filled.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 16.5L4.2 10.7C2.9 9.4 2.9 7.3 4.2 6C5.5 4.7 7.6 4.7 8.9 6L10 7.1L11.1 6C12.4 4.7 14.5 4.7 15.8 6C17.1 7.3 17.1 9.4 15.8 10.7L10 16.5Z" fill="#FF8A4C" stroke="#FF8A4C" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
3
src/static/icons/icon-heart.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 16.5L4.2 10.7C2.9 9.4 2.9 7.3 4.2 6C5.5 4.7 7.6 4.7 8.9 6L10 7.1L11.1 6C12.4 4.7 14.5 4.7 15.8 6C17.1 7.3 17.1 9.4 15.8 10.7L10 16.5Z" stroke="#999999" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
4
src/static/icons/icon-search.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="9" cy="9" r="5.5" stroke="#999999" stroke-width="1.6"/>
|
||||
<path d="M13.5 13.5L17 17" stroke="#999999" stroke-width="1.6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 263 B |
BIN
src/static/icons/invoice.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/static/icons/new-card.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/static/icons/qualification.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/static/icons/security.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/static/icons/service-app.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/static/icons/service-desktop.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/static/icons/service-mini.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/static/icons/service-secondary.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
4
src/static/icons/service-secondary.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="12" fill="#E8FAF4"/>
|
||||
<path d="M18 30L22 18L26 26L30 22L32 30" stroke="#22C55E" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 287 B |
BIN
src/static/icons/service-web.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/static/icons/tab-case-active.png
Normal file
|
After Width: | Height: | Size: 938 B |
BIN
src/static/icons/tab-case.png
Normal file
|
After Width: | Height: | Size: 991 B |
@@ -1,6 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="4" width="7" height="7" rx="1.5" stroke="#999999" stroke-width="1.8"/>
|
||||
<rect x="13" y="4" width="7" height="7" rx="1.5" stroke="#999999" stroke-width="1.8"/>
|
||||
<rect x="4" y="13" width="7" height="7" rx="1.5" stroke="#999999" stroke-width="1.8"/>
|
||||
<rect x="13" y="13" width="7" height="7" rx="1.5" stroke="#999999" stroke-width="1.8"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 459 B |
BIN
src/static/icons/tab-home-active.png
Normal file
|
After Width: | Height: | Size: 455 B |
BIN
src/static/icons/tab-home.png
Normal file
|
After Width: | Height: | Size: 507 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 10.5L12 4L20 10.5V19C20 19.5523 19.5523 20 19 20H5C4.44772 20 4 19.5523 4 19V10.5Z" stroke="#2563EB" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<path d="M9 20V13H15V20" stroke="#2563EB" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 352 B |
BIN
src/static/icons/tab-message-active.png
Normal file
|
After Width: | Height: | Size: 467 B |
BIN
src/static/icons/tab-message.png
Normal file
|
After Width: | Height: | Size: 388 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 6H19C19.5523 6 20 6.44772 20 7V15C20 15.5523 19.5523 16 19 16H9L5 19V7C5 6.44772 5.44772 6 6 6H5Z" stroke="#999999" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 278 B |
BIN
src/static/icons/tab-messages.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/static/icons/tab-personal-active.png
Normal file
|
After Width: | Height: | Size: 679 B |
BIN
src/static/icons/tab-personal.png
Normal file
|
After Width: | Height: | Size: 753 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="8" r="4" stroke="#999999" stroke-width="1.8"/>
|
||||
<path d="M5 19C5.67157 16.7909 8.13401 15 12 15C15.866 15 18.3284 16.7909 19 19" stroke="#999999" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 316 B |
BIN
src/static/icons/tab-personals.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/static/icons/tab-project-active.png
Normal file
|
After Width: | Height: | Size: 336 B |
BIN
src/static/icons/tab-project.png
Normal file
|
After Width: | Height: | Size: 364 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 7C5 5.89543 5.89543 5 7 5H10L12 7H17C18.1046 7 19 7.89543 19 9V17C19 18.1046 18.1046 19 17 19H7C5.89543 19 5 18.1046 5 17V7Z" stroke="#999999" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 305 B |
15
src/types/business-detail.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface ServiceTypeItem {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export interface BusinessDetailContent {
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
icon: string
|
||||
serviceTypes: ServiceTypeItem[]
|
||||
contentTitle: string
|
||||
contentSections: string[]
|
||||
}
|
||||
25
src/types/case-library.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export type CaseCategory = '全部' | '互联网软件' | '硬件电路' | '物联网全案' | '生产制造'
|
||||
|
||||
export interface CaseItem {
|
||||
id: string
|
||||
title: string
|
||||
category: Exclude<CaseCategory, '全部'>
|
||||
cover: string
|
||||
clientType: string
|
||||
industry: string
|
||||
scale: string
|
||||
cycle: string
|
||||
teamSize: string
|
||||
completedAt: string
|
||||
favorite?: boolean
|
||||
}
|
||||
|
||||
export interface CaseFilterValues {
|
||||
industries: string[]
|
||||
scales: string[]
|
||||
timeRanges: string[]
|
||||
dateRange: {
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export interface ServiceItem {
|
||||
desc: string
|
||||
theme: 'blue' | 'green' | 'pink' | 'orange'
|
||||
illus?: string
|
||||
detailId?: string
|
||||
}
|
||||
|
||||
export interface QuickEntryItem {
|
||||
@@ -21,5 +22,5 @@ export interface ProjectItem {
|
||||
progress: number
|
||||
status: string
|
||||
updateTime: string
|
||||
tagTheme?: 'orange' | 'peach'
|
||||
tagTheme?: 'orange'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ResultPageType = 'register' | 'resetPassword' | 'loginFailed'
|
||||
export type ResultPageType = 'register' | 'resetPassword' | 'loginFailed' | 'submitSuccess'
|
||||
|
||||
export interface ResultPageConfig {
|
||||
title: string
|
||||
@@ -26,6 +26,12 @@ export const RESULT_PAGE_CONFIG: Record<ResultPageType, ResultPageConfig> = {
|
||||
icon: '/static/icons/icon-login-failed.svg',
|
||||
redirectUrl: '/pages/Login/user-register/index',
|
||||
},
|
||||
submitSuccess: {
|
||||
title: '提报成功',
|
||||
subtitleTarget: '首页',
|
||||
icon: '/static/icons/icon-register-success.png',
|
||||
redirectUrl: '/pages/home-index/index',
|
||||
},
|
||||
}
|
||||
|
||||
export const RESULT_PAGE_ROUTE = '/pages/Login/user-register-success/index'
|
||||
|
||||
147
src/utils/case-filter.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { CaseFilterValues, CaseItem } from '@/types/case-library'
|
||||
|
||||
function parseScaleValue(scale: string): number {
|
||||
const match = scale.match(/(\d+)/)
|
||||
return match ? Number(match[1]) : 0
|
||||
}
|
||||
|
||||
function matchScaleRange(scale: string, range: string): boolean {
|
||||
const value = parseScaleValue(scale)
|
||||
|
||||
switch (range) {
|
||||
case '50万以下':
|
||||
return value < 50
|
||||
case '50-100万':
|
||||
return value >= 50 && value <= 100
|
||||
case '100-300万':
|
||||
return value > 100 && value <= 300
|
||||
case '300-500万':
|
||||
return value > 300 && value <= 500
|
||||
case '500万以上':
|
||||
return value > 500
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function parseDate(dateStr: string): Date | null {
|
||||
if (!dateStr)
|
||||
return null
|
||||
|
||||
const [year, month, day] = dateStr.split('/').map(Number)
|
||||
if (!year || !month || !day)
|
||||
return null
|
||||
|
||||
return new Date(year, month - 1, day)
|
||||
}
|
||||
|
||||
function matchTimeRange(completedAt: string, range: string, now = new Date()): boolean {
|
||||
const date = parseDate(completedAt)
|
||||
if (!date)
|
||||
return false
|
||||
|
||||
const monthsMap: Record<string, number> = {
|
||||
近3个月: 3,
|
||||
近半年: 6,
|
||||
近一年: 12,
|
||||
}
|
||||
const months = monthsMap[range]
|
||||
if (!months)
|
||||
return true
|
||||
|
||||
const start = new Date(now)
|
||||
start.setMonth(start.getMonth() - months)
|
||||
|
||||
return date >= start && date <= now
|
||||
}
|
||||
|
||||
function matchDateRange(completedAt: string, start: string, end: string): boolean {
|
||||
const date = parseDate(completedAt)
|
||||
const startDate = parseDate(start)
|
||||
const endDate = parseDate(end)
|
||||
|
||||
if (!date || !startDate || !endDate)
|
||||
return true
|
||||
|
||||
return date >= startDate && date <= endDate
|
||||
}
|
||||
|
||||
export function cloneCaseFilter(filter: CaseFilterValues): CaseFilterValues {
|
||||
return {
|
||||
industries: [...filter.industries],
|
||||
scales: [...filter.scales],
|
||||
timeRanges: [...filter.timeRanges],
|
||||
dateRange: {
|
||||
start: filter.dateRange.start,
|
||||
end: filter.dateRange.end,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function getSelectedFilterTags(filter: CaseFilterValues): Array<{ key: string, label: string }> {
|
||||
const tags: Array<{ key: string, label: string }> = []
|
||||
|
||||
filter.industries.forEach((item) => {
|
||||
tags.push({ key: `industry:${item}`, label: item })
|
||||
})
|
||||
|
||||
filter.scales.forEach((item) => {
|
||||
tags.push({ key: `scale:${item}`, label: item })
|
||||
})
|
||||
|
||||
filter.timeRanges.forEach((item) => {
|
||||
tags.push({ key: `time:${item}`, label: item })
|
||||
})
|
||||
|
||||
if (filter.dateRange.start && filter.dateRange.end) {
|
||||
tags.push({
|
||||
key: 'date-range',
|
||||
label: `${filter.dateRange.start}-${filter.dateRange.end}`,
|
||||
})
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
export function removeFilterTag(filter: CaseFilterValues, key: string): CaseFilterValues {
|
||||
const next = cloneCaseFilter(filter)
|
||||
|
||||
if (key.startsWith('industry:')) {
|
||||
const value = key.replace('industry:', '')
|
||||
next.industries = next.industries.filter(item => item !== value)
|
||||
}
|
||||
|
||||
if (key.startsWith('scale:')) {
|
||||
const value = key.replace('scale:', '')
|
||||
next.scales = next.scales.filter(item => item !== value)
|
||||
}
|
||||
|
||||
if (key.startsWith('time:')) {
|
||||
const value = key.replace('time:', '')
|
||||
next.timeRanges = next.timeRanges.filter(item => item !== value)
|
||||
}
|
||||
|
||||
if (key === 'date-range') {
|
||||
next.dateRange = { start: '', end: '' }
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
export function matchCaseFilter(item: CaseItem, filter: CaseFilterValues): boolean {
|
||||
if (filter.industries.length > 0 && !filter.industries.includes(item.industry))
|
||||
return false
|
||||
|
||||
if (filter.scales.length > 0 && !filter.scales.some(range => matchScaleRange(item.scale, range)))
|
||||
return false
|
||||
|
||||
if (filter.timeRanges.length > 0 && !filter.timeRanges.some(range => matchTimeRange(item.completedAt, range)))
|
||||
return false
|
||||
|
||||
if (filter.dateRange.start && filter.dateRange.end) {
|
||||
if (!matchDateRange(item.completedAt, filter.dateRange.start, filter.dateRange.end))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
15
src/utils/navigate-back.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function navigateBackWithFallback(fallbackUrl: string) {
|
||||
const pages = getCurrentPages()
|
||||
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
|
||||
uni.redirectTo({
|
||||
url: fallbackUrl,
|
||||
fail: () => {
|
||||
uni.reLaunch({ url: fallbackUrl })
|
||||
},
|
||||
})
|
||||
}
|
||||