完善搜索功能:

1. 添加搜索方法实现,支持关键词搜索
2. 实现取消搜索功能,清空搜索结果
3. 添加搜索结果展示组件
4. 优化搜索结果样式,使用Element Plus风格
5. 添加搜索输入验证,防止空搜索
This commit is contained in:
Huangzhe
2025-05-12 14:18:06 +08:00
parent d10ab302d5
commit a648ef27fc
23 changed files with 385 additions and 12833 deletions

View File

@@ -66,5 +66,12 @@
"stylelint.configFile": "./node_modules/@yl/yili-fe-lint-config/stylelintrc.js", // 该选项指定了 stylelint 应使用的配置文件路径。此项设置会覆盖所有其他位置查找的 stylelint 配置文件
"stylelint.customSyntax": "postcss-scss", // 配置stylelint使用的预处理器
"stylelint.stylelintPath": "./node_modules/@yl/yili-fe-lint-config/node_modules/stylelint", // 指定stylelint安装路径
"stylelint.validate": ["html", "css", "scss", "less", "vue"]
"stylelint.validate": [
"html",
"css",
"scss",
"less",
"vue"
],
"typescript.tsdk": "node_modules\\typescript\\lib"
}

4
auto-imports.d.ts vendored
View File

@@ -5,4 +5,6 @@
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {}
declare global {
}

15
components.d.ts vendored
View File

@@ -9,28 +9,38 @@ declare module 'vue' {
export interface GlobalComponents {
Contenteditable: typeof import('./src/components/contenteditable.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElCol: typeof import('element-plus/es')['ElCol']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElOption: typeof import('element-plus/es')['ElOption']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSpace: typeof import('element-plus/es')['ElSpace']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElText: typeof import('element-plus/es')['ElText']
Index: typeof import('./src/components/Navigation/Index.vue')['default']
RichText: typeof import('./src/components/RichText.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
VanActionSheet: typeof import('vant/es')['ActionSheet']
VanButton: typeof import('vant/es')['Button']
VanCard: typeof import('vant/es')['Card']
VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanCheckbox: typeof import('vant/es')['Checkbox']
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
VanCol: typeof import('vant/es')['Col']
VanDivider: typeof import('vant/es')['Divider']
VanField: typeof import('vant/es')['Field']
VanGrid: typeof import('vant/es')['Grid']
VanGridItem: typeof import('vant/es')['GridItem']
VanIcon: typeof import('vant/es')['Icon']
VanList: typeof import('vant/es')['List']
VanNavBar: typeof import('vant/es')['NavBar']
@@ -39,12 +49,13 @@ declare module 'vue' {
VanPopup: typeof import('vant/es')['Popup']
VanRadio: typeof import('vant/es')['Radio']
VanRadioGroup: typeof import('vant/es')['RadioGroup']
VanRow: typeof import('vant/es')['Row']
VanSearch: typeof import('vant/es')['Search']
VanSpace: typeof import('vant/es')['Space']
VanStepper: typeof import('vant/es')['Stepper']
VanSwitch: typeof import('vant/es')['Switch']
VanTab: typeof import('vant/es')['Tab']
VanTabs: typeof import('vant/es')['Tabs']
VanTag: typeof import('vant/es')['Tag']
YLCascader: typeof import('./src/components/YLCascader.vue')['default']
YLInput: typeof import('./src/components/YLInput.vue')['default']
YLPicker: typeof import('./src/components/YLPicker.vue')['default']

12769
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,15 +26,14 @@
"element-plus": "^2.7.8",
"js-base64": "^3.7.7",
"lodash": "^4.17.21",
"pinia": "^2.1.7",
"pinia": "^3.0.2",
"regenerator-runtime": "^0.14.1",
"shrinkpng": "^1.2.0-beta.1",
"sortablejs": "^1.15.6",
"uuid": "^11.1.0",
"vant": "^4.9.17",
"vconsole": "^3.15.1",
"vite-plugin-vue": "^0.0.1",
"vue": "^3.4.29",
"vue": "^3.5.13",
"vue-icons-plus": "^0.1.8",
"vue-router": "^4.3.3",
"vuex": "^4.1.0"
@@ -63,7 +62,7 @@
"postcss-pxtorem": "^6.1.0",
"sass": "^1.85.1",
"sass-loader": "^16.0.5",
"typescript": "^5.8.3",
"typescript": "^5.2.2",
"unplugin-auto-import": "^0.18.6",
"unplugin-vue-components": "^0.27.5",
"vite": "^6.0.0",

View File

@@ -22,6 +22,7 @@ export function getSurveysPage(params) {
params
});
}
// 复制问卷
export function copySurveys(sn) {
// source=1 表明是移动端问卷

View File

@@ -1,6 +1,6 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--primary-color: #71b73c;
--primary-color: #70B937;
--van-primary-color: #71b73c;
--vt-c-white: #fff;
--vt-c-white-soft: #f8f8f8;

View File

@@ -0,0 +1,73 @@
<script setup lang="ts">
import { AiFillHome } from 'vue-icons-plus/ai';
import { h, ref } from 'vue';
import { useRouter, type RouteLocationRaw } from 'vue-router';
const router = useRouter()
const navigation = ref([
{
title: '主页',
link: {
name: 'home',
path: '/home'
},
icon: h(AiFillHome)
},
{
title: '伊调研',
link: {
name: 'home',
path: '/home'
},
icon: h(AiFillHome)
},
{
title: '我的',
link: {
name: '',
path: '/survey'
},
icon: h(AiFillHome)
}
]);
/**
* Handle changing of navigation
* @param {object} nav - the navigation item selected
*/
function handleChangeNav(nav: any) {
const params = {} as RouteLocationRaw
// 如果有 name 表示,优先取 name 参数
nav.link.name.length != 0 ? (params.name = nav.link.name as string) : (params.path = nav.link.path)
router.push(params)
}
</script>
<template>
<div class="navigation">
<section @click="handleChangeNav(item)" v-for="item in navigation" :key="item.title" class="navigation-item">
<component :is="item.icon"></component>
<div>{{ item.title }}</div>
</section>
</div>
</template>
<style scoped lang="scss">
.navigation {
width: 100vw;
background-color: white;
display: flex;
flex-flow: row;
justify-content: space-around;
position: fixed;
bottom: 0px;
z-index: 10;
}
.navigation-item {
display: flex;
flex-flow: column nowrap;
align-items: center;
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<section class="search-container">
<van-search v-model="value" :placeholder="placeholder" @search="searchMethod" @cancel="emitCancel"
style="--van-search-padding: 0;">
</van-search>
<el-text>
搜索
</el-text>
</section>
</template>
<script setup>
import { ref } from 'vue';
const value = ref('');
/**
* @description 搜索方法
*/
const searchMethod = defineModel('search', {
type: Function,
default: () => void 0
});
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;
overflow: hidden;
}
</style>

View File

@@ -2,14 +2,8 @@
<div class="common-layout container">
<!-- title 标题和搜索栏 -->
<header class="header">
<van-nav-bar
class="navbar-header"
:title="$route.meta.title"
left-arrow
safe-area-inset-top
:border="false"
@click-left="goBack"
>
<van-nav-bar class="navbar-header" :title="$route.meta.title" left-arrow safe-area-inset-top :border="false"
@click-left="goBack">
<template #left>
<van-icon name="left-long" class-prefix="mobilefont" size="18" style="color: #fff" />
</template>
@@ -23,14 +17,13 @@
</template>
<script setup>
import { RouterView, useRouter, useRoute } from 'vue-router';
import { RouterView, useRoute, useRouter } from 'vue-router';
import appBridge from '@/assets/js/appBridge';
import { onMounted, onUnmounted } from 'vue';
const route = useRoute();
const router = useRouter();
// const router = useRouter();
function goBack() {
function goBack () {
if (window.history.length > 1 && route.name !== 'home') {
window.history.back();
} else {

View File

@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router';
import layout from '@/layouts/index.vue';
import Design from '@/views/Design/Index.vue';
import Redirect from '@/layouts/redirect.vue';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
@@ -71,12 +72,27 @@ const router = createRouter({
},
component: () => import('@/views/Survey/views/Preview/Index.vue')
},
{
path: '/publish',
name: 'publish',
meta: { title: '问卷投放' },
component: () => import('../views/Survey/views/Publish/Index.vue')
},
{
path: 'intelligentGeneration',
name: 'intelligentGeneration',
meta: {
title: '智能创建'
},
component: () =>
import(
'@/views/Home/components/CreateSurvey/components/IntelligentGeneration/Index.vue'
)
}, {
path: '/search',
name: 'search',
meta: {},
component: () => import("@/views/HomeSearch/Index.vue")
}
]
},

View File

@@ -0,0 +1,36 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useSurveyStore = defineStore('survey', () => {
const questions = ref<string[]>([]);
const answers = ref<Record<string, string>>({});
const totalQuestions = computed(() => questions.value.length);
const answeredCount = computed(() => Object.keys(answers.value).length);
const isComplete = computed(() => answeredCount.value === totalQuestions.value);
function addQuestion(question: string) {
questions.value.push(question);
}
function answerQuestion(question: string, answer: string) {
if (questions.value.includes(question)) {
answers.value[question] = answer;
}
}
function resetSurvey() {
answers.value = {};
}
return {
questions,
answers,
totalQuestions,
answeredCount,
isComplete,
addQuestion,
answerQuestion,
resetSurvey,
};
});

View File

@@ -4,7 +4,7 @@ import { showToast } from 'vant';
// import router from '@/router/index';
// import { A_COMMON_CLEAR_TOKEN } from '@/stores/constance/constance.common.js';
import * as config from '@/config.js';
import * as config from '@/config';
// import {proxyUrl} from config.default
// const NODE_ENV = import.meta.env.VITE_APP_ENV;
const baseURL = config.default.proxyUrl;
@@ -25,7 +25,14 @@ service.interceptors.request.use(
if (!config.headers) {
config.headers.Accept = 'application/json';
}
config.headers.Authorization = `${localStorage.getItem('plantToken')}`;
let plantToken = localStorage.getItem('plantToken');
// 如果 token 不存在, 试图尝试让用户输入 token ,然后放入本地 token 内容中
// 仅限开发环境中
if (!plantToken && !import.meta.env.PROD) {
plantToken = prompt('token 不存在,请输入 plant token');
localStorage.setItem('plantToken');
}
config.headers.Authorization = `${plantToken}`;
config.headers.Source = 1;
// if (!config.headers.remoteIp) {
// config.baseURL += '/api';

View File

@@ -10,10 +10,11 @@ import { showFailToast } from 'vant';
import appBridge from '@/assets/js/appBridge';
import ImageSlider from './components/ImageSlider/Index.vue';
import MineTask from '@/views/Home/components/MineTask/Index.vue';
import Navigation from "@/components/Navigation/Index.vue"
const contentShow = ref(false);
onMounted(async () => {
onMounted(async() => {
if (appBridge.isInReactNative()) {
const appToken = utils.getSessionStorage('xToken');
getUserInfo(appToken)
@@ -44,17 +45,19 @@ onMounted(async () => {
<template>
<div v-if="contentShow" class="container-home">
<div class="container-body">
<create-survey :createdNewPage="false" />
<create-survey :createdNewPage="false"/>
<!-- 首页轮播图 -->
<image-slider />
<image-slider/>
<!-- 最新问卷 -->
<!-- <last-survey />-->
<!--<last-survey/>-->
<!-- 模板市场 -->
<!-- <Market />-->
<Market/>
<!--底部新建问卷-->
<NewSurvey />
<NewSurvey/>
<mine-task />
<mine-task/>
<navigation/>
</div>
</div>
</template>

View File

@@ -112,6 +112,7 @@ onMounted(() => {
<img :src="homePen" alt="" />
</div>
<div class="surveys">
<div @click="$router.push({ name: 'intelligentGeneration' })">AI 智能创建</div>
<div
v-for="survey in surveys"
:key="survey.title"

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
const url =
'https://yiligpt.x.digitalyili.com/aiagent/assistant/78907182-cc42-4072-abae-86ef67c1ecd3/share?token=123123&source=app';
</script>
<template>
<iframe style="height: 100%; width: 100%" :src="url" frameborder="0"></iframe>
</template>
<style scoped lang="scss"></style>

View File

@@ -4,9 +4,11 @@
<el-carousel-item v-for="item in sliderItems" :key="item.id">
<el-row :gutter="20" justify="space-between">
<el-col :span="16">
<el-row >
<el-row>
<el-col :span="4">
<el-icon :size="24" color="#409EFF"><DataAnalysis /></el-icon>
<el-icon :size="24" color="#409EFF">
<DataAnalysis />
</el-icon>
</el-col>
<el-col :span="20">
<h3>{{ item.title }}</h3>
@@ -18,8 +20,6 @@
</el-carousel-item>
</el-carousel>
</el-card>
</template>
<script setup lang="ts">

View File

@@ -2,7 +2,7 @@
import { ref, useTemplateRef } from 'vue';
import { showToast } from 'vant';
import { useSetPieChart } from '@/hooks/chart/usePieChart';
import { getSurveysPage } from "@/api/home"
// 饼图 dom 结构
const pieChart = useTemplateRef<HTMLSpanElement>('pieChart');
@@ -24,10 +24,26 @@ const taskData = ref({
// 图表数据
const chartData = ref([
{ name: '选项1', value: 66.77, color: '#F56C6C' },
{ name: '选项2', value: 33.33, color: '#F7BA2A' },
{ name: '选项3', value: 0, color: '#67C23A' },
{ name: '选项4', value: 0, color: '#409EFF' }
{
name: '选项1',
value: 66.77,
color: '#F56C6C'
},
{
name: '选项2',
value: 33.33,
color: '#F7BA2A'
},
{
name: '选项3',
value: 0,
color: '#67C23A'
},
{
name: '选项4',
value: 0,
color: '#409EFF'
}
]);
// 导航按钮点击事件
@@ -38,6 +54,14 @@ const handlePrev = () => {
const handleNext = () => {
showToast('点击了下一个问题');
};
getSurveysPage({
page: 1,
per_page: 3,
group_id: 0
}).then(res => {
console.log(res)
})
</script>
<template>
@@ -50,9 +74,9 @@ const handleNext = () => {
<!-- 标题部分 -->
<template #title>
<van-space>
<span style="font-size: 18px; font-weight: bold">{{ taskData.title }}</span>
<span style="color: #409eff">{{ taskData.progress }}%</span>
<span style="color: #606266; font-size: 14px">| {{ taskData.deadline }}</span>
<span style=" font-size: 18px; font-weight: bold">{{ taskData.title }}</span>
<span style=" color: #409eff">{{ taskData.progress }}%</span>
<span style=" color: #606266; font-size: 14px">| {{ taskData.deadline }}</span>
</van-space>
</template>
@@ -71,43 +95,43 @@ const handleNext = () => {
<!-- 卡片内容 -->
<template #bottom>
<van-space direction="vertical" fill style="width: 100%; padding-top: 16px">
<van-space direction="vertical" fill style=" width: 100%; padding-top: 16px">
<!-- 任务统计 -->
<van-grid :column-num="3">
<van-grid-item>
<van-space direction="vertical" fill>
<span style="color: #67c23a; font-size: 18px; font-weight: bold">{{
<span style=" color: #67c23a; font-size: 18px; font-weight: bold">{{
taskData.responseCount
}}</span>
<span style="font-size: 14px; color: #909399">回收数量</span>
}}</span>
<span style=" font-size: 14px; color: #909399">回收数量</span>
</van-space>
</van-grid-item>
<van-grid-item>
<van-space direction="vertical" fill>
<span style="color: #67c23a; font-size: 18px; font-weight: bold">{{
<span style=" color: #67c23a; font-size: 18px; font-weight: bold">{{
taskData.responseRate
}}</span>
<span style="font-size: 14px; color: #909399">回收数量进度</span>
}}</span>
<span style=" font-size: 14px; color: #909399">回收数量进度</span>
</van-space>
</van-grid-item>
<van-grid-item>
<van-space direction="vertical" fill>
<span style="color: #67c23a; font-size: 18px; font-weight: bold">{{
<span style=" color: #67c23a; font-size: 18px; font-weight: bold">{{
taskData.submissionRate
}}</span>
<span style="font-size: 14px; color: #909399">投放时间进度</span>
}}</span>
<span style=" font-size: 14px; color: #909399">投放时间进度</span>
</van-space>
</van-grid-item>
</van-grid>
<!-- 问题部分 -->
<van-divider />
<van-divider/>
<p>1. 能描述您为自己或家人购买这款产品A的可能性吗单选</p>
<!-- 图表部分 -->
<div style="display: flex; justify-content: center; margin: 16px 0">
<span ref="pieChart" style="width: 100%; height: 300px"></span>
<div style=" display: flex; justify-content: center; margin: 16px 0">
<span ref="pieChart" style=" width: 100%; height: 300px"></span>
</div>
<!-- 选项列表 -->

View File

@@ -1,15 +1,24 @@
<template>
<div class="new_survey">
<van-button type="primary" block color="#70B937" @click="create">
<span class="fw-bold">+</span> 新建问卷
</van-button>
<van-popup v-model:show="show" round closeable position="bottom" teleport="#app">
<create-question :createdNewPage="false"></create-question>
</van-popup>
<div v-if="$route.name !== 'home'">
<van-button type="primary" block color="#70B937" @click="create">
<span class="fw-bold">+</span> 新建问卷
</van-button>
<van-popup v-model:show="show" round closeable position="bottom" teleport="#app">
<create-question :createdNewPage="false"></create-question>
</van-popup>
</div>
<navigation/>
</div>
</template>
<script setup>
import CreateQuestion from '@/views/Home/components/CreateSurvey/CreateQuestion.vue';
import Navigation from "@/components/Navigation/Index.vue"
import {useRoute } from "vue-router"
const route = useRoute()
console.log(route.name)
import { ref } from 'vue';
function create() {
show.value = true;

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
import { ref } from 'vue'
import Search from '@/components/Search/Index.vue'
import { ElMessage } from 'element-plus'
interface SearchItem {
id: number
name: string
}
const searchResult = ref<SearchItem[]>([])
const searchMethod = (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 = []
}
</script>
<template>
<section>
<Search
placeholder="请输入搜索关键词"
@search="searchMethod"
@cancel="emitCancel"
show-action
/>
<div class="search-result">
<div class="search-result-item" v-for="item in searchResult" :key="item.id">
{{ item.name }}
</div>
</div>
</section>
</template>
<style scoped lang="scss">
.search-result {
padding: 16px;
&-item {
padding: 12px;
margin-bottom: 8px;
background-color: #f5f7fa;
border-radius: 4px;
font-size: 14px;
}
}
</style>

View File

@@ -26,6 +26,7 @@
<b v-html="item.project_name"></b>
</el-text>
<el-text>{{ item.answer_num }}</el-text>
<el-text size="small" v-if=item.is_time>{{`${item.start_time}${item.end_time ?? '无限期'}`}}</el-text>
</div>
<div class="survey_item_info_status">
<el-space spacer="|">
@@ -64,7 +65,19 @@
</div>
<!--问卷描述-->
<div v-if="item.introduction">
<div v-html="item.introduction" class="survey_item_info_desc"></div>
<!--<div v-html="item.introduction" class="survey_item_info_desc"></div>-->
<section>
<el-text size="small" >回收数量</el-text>
{{item.answer_num}}
</section>
<section >
<el-text size="small">回收数量进度</el-text>
{{item.recycle_progress}}
</section>
<section >
<el-text size="small">投放时间进度 </el-text>
{{ item.recycle_time }}
</section>
</div>
</div>
<!-- action 功能位置 -->

View File