feat(search): 实现首页搜索功能与模板市场组件
- 新增首页搜索功能,支持关键词搜索问卷 - 新增布局组件(CommonLayout),优化页面结构 - 新增问卷列表组件(MineSurvey),展示用户问卷 - 新增模板市场组件(TemplateMarketItem),使用Tailwind CSS样式 - 优化路由配置,添加模板市场路由 - 修复CSS样式问题,使用Tailwind CSS替代自定义样式 - 改进组件间通信,使用Vue3 defineModel API 相关任务: #TASK-2025-05-13
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -32,7 +32,7 @@
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "Vue.volar",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"css.validate": false, //用来校验CSS文件中的语法错误和潜在的问题
|
||||
|
||||
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -7,6 +7,7 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
CommonLayout: typeof import('./src/components/Layout/CommonLayout.vue')['default']
|
||||
Contenteditable: typeof import('./src/components/contenteditable.vue')['default']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
@@ -31,6 +32,7 @@ declare module 'vue' {
|
||||
RichText: typeof import('./src/components/RichText.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SimpleLayout: typeof import('./src/components/Layout/SimpleLayout.vue')['default']
|
||||
VanActionSheet: typeof import('vant/es')['ActionSheet']
|
||||
VanButton: typeof import('vant/es')['Button']
|
||||
VanCard: typeof import('vant/es')['Card']
|
||||
|
||||
@@ -18,18 +18,21 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@tailwindcss/vite": "^4.1.6",
|
||||
"axios": "^1.8.2",
|
||||
"core-js": "^3.41.0",
|
||||
"cos-js-sdk-v5": "^1.8.7",
|
||||
"dotenv": "^16.4.7",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.7.8",
|
||||
"install": "^0.13.0",
|
||||
"js-base64": "^3.7.7",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^3.0.2",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"shrinkpng": "^1.2.0-beta.1",
|
||||
"sortablejs": "^1.15.6",
|
||||
"tailwindcss": "^4.1.6",
|
||||
"uuid": "^11.1.0",
|
||||
"vant": "^4.9.17",
|
||||
"vconsole": "^3.15.1",
|
||||
|
||||
1
src/assets/css/main.css
Normal file
1
src/assets/css/main.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
@@ -1,5 +1,3 @@
|
||||
// main.scss
|
||||
|
||||
/* eslint-disable */
|
||||
@import 'theme';
|
||||
|
||||
|
||||
23
src/components/Layout/CommonLayout.vue
Normal file
23
src/components/Layout/CommonLayout.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const title = defineModel<string>('title', { default: '' });
|
||||
|
||||
const show = ref<boolean>(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-layout px-2 pt-1">
|
||||
<!-- title 部分 -->
|
||||
<section v-if="title" class="pb-1">
|
||||
<header>{{ title }}</header>
|
||||
</section>
|
||||
<slot class="" name="content" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.common-layout {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,25 +1,27 @@
|
||||
<template>
|
||||
<section class="search-container">
|
||||
<van-search v-model="value" :placeholder="placeholder" @search="searchMethod" @cancel="emitCancel"
|
||||
style="--van-search-padding: 0;">
|
||||
<van-search
|
||||
v-model="value"
|
||||
:placeholder="placeholder"
|
||||
style="--van-search-padding: 0"
|
||||
@search="searchMethod"
|
||||
>
|
||||
</van-search>
|
||||
<el-text>
|
||||
搜索
|
||||
</el-text>
|
||||
<el-text @click="searchMethod">搜索</el-text>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const value = ref('');
|
||||
<script setup lang="ts">
|
||||
// 搜索的关键词
|
||||
const value = defineModel<string>('value', { required: true });
|
||||
/**
|
||||
* @description 搜索方法
|
||||
*/
|
||||
const searchMethod = defineModel('search', {
|
||||
type: Function,
|
||||
default: () => void 0
|
||||
default: () => ({})
|
||||
});
|
||||
|
||||
const placeholder = defineModel('placeholder', {
|
||||
type: String,
|
||||
default: '请输入搜索关键词'
|
||||
@@ -44,7 +46,7 @@ const placeholder = defineModel('placeholder', {
|
||||
}
|
||||
|
||||
border-radius: 18px;
|
||||
border : solid 2px var(--primary-color);
|
||||
border: solid 2px var(--primary-color);
|
||||
background-color: #f5f5f5;
|
||||
|
||||
//width: 100vw;
|
||||
|
||||
60
src/components/Search/Index.vue~
Normal file
60
src/components/Search/Index.vue~
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<section class="search-container">
|
||||
<van-search
|
||||
v-model="value"
|
||||
:placeholder="placeholder"
|
||||
style="--van-search-padding: 0"
|
||||
@search="searchMethod"
|
||||
@update:model-value="updateValue"
|
||||
>
|
||||
</van-search>
|
||||
<el-text @click="searchMethod">搜索</el-text>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 搜索的关键词
|
||||
const value = defineModel<string>('value', { required: true });
|
||||
/**
|
||||
* @description 搜索方法
|
||||
*/
|
||||
const searchMethod = defineModel('search', {
|
||||
type: Function,
|
||||
default: () => ({})
|
||||
});
|
||||
|
||||
const placeholder = defineModel('placeholder', {
|
||||
type: String,
|
||||
default: '请输入搜索关键词'
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-container {
|
||||
:deep(.van-search) {
|
||||
padding: 0;
|
||||
margin: 0 10px;
|
||||
border-radius: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
:deep(.el-text) {
|
||||
margin: 0 10px;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
border-radius: 18px;
|
||||
border: solid 2px var(--primary-color);
|
||||
background-color: #f5f5f5;
|
||||
|
||||
//width: 100vw;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 60px;
|
||||
padding: 0 10px;
|
||||
margin: 0 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
22
src/components/SurveyItem/Index.vue
Normal file
22
src/components/SurveyItem/Index.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<h1>{{ survey.project_name }}</h1>
|
||||
<el-space spacer="|" direction="horizontal">
|
||||
<section class="flex items-center pt-1">
|
||||
<fc-businessman /> <el-text>{{ survey.created_user }}</el-text>
|
||||
</section>
|
||||
<section class="flex items-center pt-1">
|
||||
<fc-clock /> <el-text>创建时间{{ survey.created_at }}</el-text>
|
||||
</section>
|
||||
</el-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FcClock, FcBusinessman } from 'vue-icons-plus/fc';
|
||||
|
||||
const survey = defineModel<object>('survey', { required: true });
|
||||
// console.log(survey.value);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
74
src/components/TemplateMarketItem/Index.vue
Normal file
74
src/components/TemplateMarketItem/Index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<section class="w-full">
|
||||
<van-list :finished="finished" @load="onLoad">
|
||||
<van-card
|
||||
v-for="template in templates"
|
||||
:key="template.id"
|
||||
class="mb-3 rounded-sm overflow-hidden shadow-md"
|
||||
>
|
||||
<template #tags>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h3 class="m-0 font-bold text-gray-800">{{ template.title }}</h3>
|
||||
<el-tag size="small" type="success">{{ template.type }}</el-tag>
|
||||
</div>
|
||||
|
||||
<el-space spacer="|" direction="horizontal" class="mb-3 text-sm text-gray-600">
|
||||
<section class="flex items-center">
|
||||
<el-icon><User /></el-icon>
|
||||
<el-text class="ml-1">{{ template.author }}</el-text>
|
||||
</section>
|
||||
<section class="flex items-center">
|
||||
<el-icon><View /></el-icon>
|
||||
<el-text class="ml-1">{{ template.views }}次使用</el-text>
|
||||
</section>
|
||||
</el-space>
|
||||
</div>
|
||||
</template>
|
||||
</van-card>
|
||||
</van-list>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { User, View } from '@element-plus/icons-vue';
|
||||
|
||||
// 模拟模板数据
|
||||
const templates = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '报名登到模板A',
|
||||
type: '报名表',
|
||||
author: '张强',
|
||||
views: 4,
|
||||
created_at: '2025-05-01'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '客户满意度调查',
|
||||
type: '满意度',
|
||||
author: '李明',
|
||||
views: 12,
|
||||
created_at: '2025-04-28'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '产品反馈收集',
|
||||
type: '反馈',
|
||||
author: '王华',
|
||||
views: 8,
|
||||
created_at: '2025-04-25'
|
||||
}
|
||||
]);
|
||||
|
||||
// 列表加载状态
|
||||
const loading = ref(false);
|
||||
const finished = ref(true);
|
||||
|
||||
// 加载更多数据
|
||||
const onLoad = () => {
|
||||
loading.value = false;
|
||||
finished.value = true;
|
||||
};
|
||||
</script>
|
||||
@@ -6,14 +6,14 @@ import { createPinia } from 'pinia';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import utils from '@/assets/js/common';
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
// 2. 引入组件样式
|
||||
import 'vant/lib/index.css';
|
||||
|
||||
// 2. 引入组件样式, 由于引入了自动引入插件,无需手动引入
|
||||
// import 'vant/lib/index.css';
|
||||
import '@/style/utils.scss';
|
||||
import appBridge from '@/assets/js/appBridge';
|
||||
import VConsole from 'vconsole';
|
||||
import './assets/css/main.scss';
|
||||
import './assets/css/main.css';
|
||||
const app = createApp(App);
|
||||
|
||||
if (import.meta.env.VITE_APP_ENV !== 'production') {
|
||||
|
||||
@@ -92,7 +92,11 @@ const router = createRouter({
|
||||
path: '/search',
|
||||
name: 'search',
|
||||
meta: {},
|
||||
component: () => import("@/views/HomeSearch/Index.vue")
|
||||
component: () => import('@/views/HomeSearch/Index.vue')
|
||||
}, {
|
||||
path: '/template-market',
|
||||
name: 'templateMarket',
|
||||
component: () => import('@/views/Home/components/Market/Index.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -60,7 +60,7 @@ onMounted(() => {
|
||||
background-color: #70b937;
|
||||
}
|
||||
|
||||
:deep(.van-tab--active ) {
|
||||
:deep(.van-tab--active) {
|
||||
color: #000;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
8
src/views/HomeSearch/Hooks/useHomeSearch.ts
Normal file
8
src/views/HomeSearch/Hooks/useHomeSearch.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {ref} from 'vue';
|
||||
|
||||
const visible = ref<{ [key: string]: boolean }>({
|
||||
mineSurvey: true,
|
||||
templateMarket: true
|
||||
});
|
||||
|
||||
export { visible };
|
||||
80
src/views/HomeSearch/Hooks/useSurveySearch.ts
Normal file
80
src/views/HomeSearch/Hooks/useSurveySearch.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { getSurveysPage } from '@/api/home';
|
||||
|
||||
// 问卷
|
||||
const surveys = ref([]);
|
||||
// 搜索关键词
|
||||
const keyword = ref<string>();
|
||||
// 搜索页面索引, 默认第一页
|
||||
const index = ref<number>(1);
|
||||
// 是否到达最后的列表
|
||||
const lastIndex = ref<boolean>(false);
|
||||
// 每页的结果个数
|
||||
const pageCount = ref<number>(5);
|
||||
// 数组是否是脏数据,当 keyword 变动的时候,数组里面的数据已经不能用了, 需要重新获取
|
||||
// 这个是判断问卷数据是否重新获取的关键字
|
||||
const dirty = ref(true);
|
||||
// loading 状态
|
||||
const loading = ref(false);
|
||||
|
||||
async function handleSearch() {
|
||||
// loading 状态开启
|
||||
// loading.value = true;
|
||||
const params = {
|
||||
page: index.value,
|
||||
per_page: pageCount.value,
|
||||
group_id: 0,
|
||||
project_name: keyword.value
|
||||
};
|
||||
const res = await getSurveysPage(params);
|
||||
// 排除返回出错的异常
|
||||
if (res.data.code !== 0) return;
|
||||
|
||||
// 更新问卷信息
|
||||
// surveys.value = dirty ? res.data.data : surveys.value.concat(res.data.data);
|
||||
surveys.value = surveys.value.concat(res.data.data);
|
||||
|
||||
// 数据更新完成之后,脏数据标记清空
|
||||
dirty.value = false;
|
||||
// 检查是否是最后一页
|
||||
lastIndex.value = res.data.meta.last_page === index.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 搜索关键字信息
|
||||
*/
|
||||
async function updateKeyword() {
|
||||
// 排除边界条件
|
||||
if (!keyword.value) return;
|
||||
|
||||
// 重新获取数据
|
||||
await handleSearch();
|
||||
// 处理后置条件,点击搜索之后,索引更新,
|
||||
index.value = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function updatePageCount(value: number) {
|
||||
pageCount.value = value;
|
||||
}
|
||||
|
||||
// 当 keyword 变动的时候,标记脏数据
|
||||
watch(keyword, async () => {
|
||||
dirty.value = true;
|
||||
// 重新获取数据
|
||||
// await handleSearch();
|
||||
});
|
||||
|
||||
// 索引变动之后,立刻进行搜索 (没有进行防抖)
|
||||
watch(index, async () => {
|
||||
console.log(`index add`, index.value, loading.value);
|
||||
// 当不在 loading 的状态时候再开始获取数据
|
||||
await nextTick(() => {
|
||||
handleSearch();
|
||||
});
|
||||
});
|
||||
|
||||
export { keyword, updateKeyword as handleSearch, lastIndex, loading, surveys, index };
|
||||
@@ -1,48 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Search from '@/components/Search/Index.vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TemplateMarket from "@/views/HomeSearch/components/TemplateMarket/Index.vue"
|
||||
|
||||
interface SearchItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const searchResult = ref<SearchItem[]>([]);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
if (!value.trim()) {
|
||||
ElMessage.warning('请输入搜索关键词');
|
||||
return;
|
||||
}
|
||||
|
||||
// 这里模拟搜索结果,实际应用中应该调用 API
|
||||
searchResult.value = [
|
||||
{ id: 1, name: `搜索结果1: ${value}` },
|
||||
{ id: 2, name: `搜索结果2: ${value}` },
|
||||
{ id: 3, name: `搜索结果3: ${value}` }
|
||||
];
|
||||
};
|
||||
|
||||
const emitCancel = () => {
|
||||
searchResult.value = [];
|
||||
};
|
||||
import TemplateMarket from '@/views/HomeSearch/components/TemplateMarket/Index.vue';
|
||||
import MineSurvey from '@/views/HomeSearch/components/MineSurvey/Index.vue';
|
||||
import { handleSearch, keyword } from '@/views/HomeSearch/Hooks/useSurveySearch';
|
||||
import Layout from '@/components/Layout/CommonLayout.vue';
|
||||
import { visible } from '@/views/HomeSearch/Hooks/useHomeSearch';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<Search placeholder="请输入搜索关键词" :search="handleSearch" />
|
||||
<search v-model:value="keyword" :search="handleSearch" />
|
||||
<!-- 广告区域 -->
|
||||
|
||||
<!-- 我的问卷区域 -->
|
||||
<layout v-if="visible.mineSurvey" title="我的任务">
|
||||
<template #content>
|
||||
<mine-survey />
|
||||
</template>
|
||||
</layout>
|
||||
<!-- 更多模板区域 -->
|
||||
<template-market />
|
||||
|
||||
<div class="search-result">
|
||||
<div class="search-result-item" v-for="item in searchResult" :key="item.id">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<layout v-if="visible.templateMarket" title="问卷模板">
|
||||
<template #content>
|
||||
<template-market />
|
||||
</template>
|
||||
</layout>
|
||||
<!-- <div class="search-result">-->
|
||||
<!-- <div class="search-result-item" v-for="item in searchResult" :key="item.id">-->
|
||||
<!-- {{ item.name }}-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
34
src/views/HomeSearch/components/MineSurvey/Index.vue
Normal file
34
src/views/HomeSearch/components/MineSurvey/Index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { index, surveys, lastIndex, keyword } from '@/views/HomeSearch/Hooks/useSurveySearch';
|
||||
import SurveyItem from '@/components/SurveyItem/Index.vue';
|
||||
|
||||
/**
|
||||
* van-list 触发 loading 状态函数
|
||||
*/
|
||||
function handleLoadStatus() {
|
||||
// 关键字为空时不进行搜索
|
||||
if (!keyword.value) return;
|
||||
index.value += 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="survey-container">
|
||||
<van-list :offset="10" :finished="lastIndex" @load="handleLoadStatus">
|
||||
<van-card v-for="survey in surveys" :key="survey" class="rounded-xs">
|
||||
<template #tags>
|
||||
<survey-item :survey="survey" />
|
||||
</template>
|
||||
</van-card>
|
||||
</van-list>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.survey-container {
|
||||
min-height: 300px;
|
||||
height: 30vh;
|
||||
overflow: scroll;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import MarketItem from '@/components/MarketItem/MarketItem.vue';
|
||||
|
||||
// import MarketItem from '@/components/MarketItem/MarketItem.vue';
|
||||
import MarketItem from '@/components/TemplateMarketItem/Index.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<market-item info=""></market-item>
|
||||
<market-item></market-item>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -10,6 +10,7 @@ import postCssPxToRem from 'postcss-pxtorem';
|
||||
import legacy from '@vitejs/plugin-legacy';
|
||||
// shift + alt 快速定位到对应组件
|
||||
import { codeInspectorPlugin } from 'code-inspector-plugin';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
// 接收 mode 参数
|
||||
@@ -74,8 +75,9 @@ export default defineConfig(({ mode }) => {
|
||||
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
|
||||
}),
|
||||
codeInspectorPlugin({
|
||||
bundler: 'vite',
|
||||
bundler: 'vite'
|
||||
}),
|
||||
tailwindcss()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user