问卷模块优化与全面项目结构调整:

1. 类型定义:
   - 完善 SurveyItem 类型定义,添加所有必要字段
   - 优化类型注解,增强代码可维护性
   - 更新 components.d.ts 类型声明文件

2. 组件重构:
   - 将 SuvreyItem 组件变量名从 item 统一修改为 survey
   - 添加类型标注,提高代码健壮性
   - 修复模板中的变量引用
   - 优化 MarketItem、Navigation 和 Search 组件

3. 功能优化:
   - 将删除和保存模板功能从 Index.vue 移至 useSurveyData.ts 钩子中
   - 优化函数导出,提高代码复用性
   - 改进 Home 组件与问卷模块的交互逻辑

4. 样式优化:
   - 精简 SCSS 代码,移除冗余样式和注释
   - 将样式从 Index.vue 移至 SuvreyItem.vue 组件内部
   - 优化样式结构,提高可维护性

5. 资源添加:
   - 新增 tabbar 图标资源,统一底部导航栏样式
This commit is contained in:
Huangzhe
2025-05-13 15:45:25 +08:00
parent 36946f632f
commit e87a064156
16 changed files with 612 additions and 485 deletions

3
components.d.ts vendored
View File

@@ -17,6 +17,7 @@ declare module 'vue' {
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElOption: typeof import('element-plus/es')['ElOption']
@@ -59,6 +60,8 @@ declare module 'vue' {
VanStepper: typeof import('vant/es')['Stepper']
VanSwitch: typeof import('vant/es')['Switch']
VanTab: typeof import('vant/es')['Tab']
VanTabbar: typeof import('vant/es')['Tabbar']
VanTabbarItem: typeof import('vant/es')['TabbarItem']
VanTabs: typeof import('vant/es')['Tabs']
VanTag: typeof import('vant/es')['Tag']
YLCascader: typeof import('./src/components/YLCascader.vue')['default']

BIN
public/tabbar/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

BIN
public/tabbar/mine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

BIN
public/tabbar/yl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,54 +1,51 @@
<template>
<div class="">
<div class="mark_container">
<!-- <van-row gutter="20">-->
<div
v-for="(item, index) in info"
:key="index"
class="market-item"
@click.stop="toDetail(item)"
>
<div class="content">
<div class="title fw-bold fs-14">
<div class="flex align-center">
<img
:src="setImg(item.scene_code_info)"
alt="Content Icon"
style="width: 15px; height: 15px"
/>
<p class="title_con">{{ item.title }}</p>
</div>
<van-icon
v-if="userInfo?.userName === item.created_user"
name="delete1"
class-prefix="mobilefont"
size="18"
style="color: #ff6214"
@click.stop="deleteItem(item)"
/>
<van-icon
v-else
name="delete1"
class-prefix="mobilefont"
size="18"
style="color: #c1c1c1"
<div class="mark_container">
<!-- <van-row gutter="20">-->
<div
v-for="(item, index) in info"
:key="index"
class="market-item"
@click.stop="toDetail(item)"
>
<div class="content">
<div class="title fw-bold fs-14">
<div class="flex align-center">
<img
:src="setImg(item.scene_code_info)"
alt="Content Icon"
style="width: 15px; height: 15px"
/>
<p class="title_con">{{ item.title }}</p>
</div>
<div class="desc flex space-between align-center">
<div>
<p>创建人</p>
<p>{{ item.created_user }}</p>
</div>
<div class="line"></div>
<div>
<p>引用次数</p>
<p>{{ item.quote_nums }}</p>
</div>
<van-icon
v-if="userInfo?.userName === item.created_user"
name="delete1"
class-prefix="mobilefont"
size="18"
style="color: #ff6214"
@click.stop="deleteItem(item)"
/>
<van-icon
v-else
name="delete1"
class-prefix="mobilefont"
size="18"
style="color: #c1c1c1"
/>
</div>
<div class="desc flex space-between align-center">
<div>
<p>创建人</p>
<p>{{ item.created_user }}</p>
</div>
<div class="line"></div>
<div>
<p>引用次数</p>
<p>{{ item.quote_nums }}</p>
</div>
</div>
</div>
<!-- </van-row>-->
</div>
</div>
</template>
@@ -135,13 +132,15 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
$gap: 10px;
.mark_container {
display: grid;
grid-gap: 5px;
grid-gap: $gap;
grid-template-columns: repeat(2, 1fr);
//flex-wrap: wrap;
//justify-content: space-between;
padding: 10px 0;
padding: $gap;
}
.market-item {
@@ -153,7 +152,7 @@ onMounted(() => {
padding: 15px 12px;
border: 1px solid #fff;
border-radius: 10px;
background: rgba(241, 241, 241, 0.39);
background: #fff;
&:nth-child(n-1) {
margin-right: 7px;

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { AiFillHome } from 'vue-icons-plus/ai';
import { h, ref } from 'vue';
import { useRouter, type RouteLocationRaw } from 'vue-router';
import { ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const router = useRouter()
const router = useRouter();
const route = useRoute();
const navigation = ref([
{
@@ -12,7 +12,7 @@ const navigation = ref([
name: 'home',
path: '/home'
},
icon: h(AiFillHome)
icon: '/tabbar/home.png'
},
{
title: '伊调研',
@@ -20,7 +20,7 @@ const navigation = ref([
name: 'home',
path: '/home'
},
icon: h(AiFillHome)
icon: '/tabbar/yl.png'
},
{
title: '我的',
@@ -28,28 +28,42 @@ const navigation = ref([
name: '',
path: '/survey'
},
icon: h(AiFillHome)
icon: '/tabbar/mine.png'
}
]);
// 当前的 nav
const currentNav = navigation.value.find((item) => item.link.path === route.fullPath);
const activeTab = ref(currentNav?.title);
/**
* Handle changing of navigation
* @param {object} nav - the navigation item selected
*/
function handleChangeNav(nav: any) {
const params = {} as RouteLocationRaw
function handleChangeNav(title: string) {
const nav = navigation.value.find((item) => item.title === title);
const params: { [key: string]: string } = {};
// 如果有 name 表示,优先取 name 参数
nav.link.name.length != 0 ? (params.name = nav.link.name as string) : (params.path = nav.link.path)
router.push(params)
nav && nav.link.name.length !== 0
? (params.name = nav.link.name as string)
: (params.path = nav?.link.path as string);
router.push(params);
}
watch(activeTab, (value: string) => {
handleChangeNav(value);
});
</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>
<van-tabbar v-model="activeTab">
<van-tabbar-item v-for="item in navigation" :key="item.title" :name="item.title">
<template #icon>
<img :src="item.icon" />
</template>
{{ item.title }}
</van-tabbar-item>
</van-tabbar>
</div>
</template>

View File

@@ -17,9 +17,8 @@ const value = defineModel<string>('value', { required: true });
/**
* @description 搜索方法
*/
const searchMethod = defineModel('search', {
type: Function,
default: () => ({})
const searchMethod = defineModel<any>('search', {
type: Function
});
const placeholder = defineModel('placeholder', {

View File

@@ -1,9 +1,15 @@
<template>
<div class="common-layout container">
<div class="common-layout">
<!-- 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>
@@ -17,13 +23,13 @@
</template>
<script setup>
import { RouterView, useRoute, useRouter } from 'vue-router';
import { RouterView, useRoute } from 'vue-router';
import appBridge from '@/assets/js/appBridge';
const route = useRoute();
// const router = useRouter();
function goBack () {
function goBack() {
if (window.history.length > 1 && route.name !== 'home') {
window.history.back();
} else {
@@ -53,7 +59,7 @@ const handlePopState = () => {
.common-layout {
overflow: hidden;
height: 100vh;
background-color: white;
background-color: #f3f3f3;
color: #333;
}

View File

@@ -10,11 +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"
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)
@@ -45,19 +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/>-->
<!-- 模板市场 -->
<Market/>
<!-- <Market/> -->
<!--底部新建问卷-->
<NewSurvey/>
<NewSurvey />
<mine-task/>
<mine-task />
<navigation/>
<navigation />
</div>
</div>
</template>

View File

@@ -1,25 +1,34 @@
<template>
<!-- 模板 -->
<div class="market">
<div class="market_title fw-bold">模板市场</div>
<van-tabs v-model:active="active" @change="getMarketInfo">
<van-tab v-for="item in marketList" :key="item.title" :title="item.h5Title">
<market-item :info="marketInfo" :marketItem="item" />
</van-tab>
<search></search>
<van-tabs v-model:active="active" class="px-1" @change="getMarketInfo">
<section>
<van-tab v-for="item in marketList" :key="item.title" :title="item.h5Title">
<template #title>
<section>{{ item.title }}</section>
</template>
</van-tab>
</section>
</van-tabs>
</div>
<market-item :info="marketInfo" :marketItem="marketItem" />
<div class="more">
<p>-更多模板期待您的探索-</p>
</div>
</template>
<script setup>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import MarketItem from '@/components/MarketItem/MarketItem.vue';
import { getListScene, getSurveyTemplates } from '@/api/home';
import Search from '@/components/Search/Index.vue';
const marketList = ref([]);
const active = ref(null);
const marketIndex = ref(0);
const marketInfo = ref([]);
// 当前激活的 item 信息
const marketItem = ref();
const getTableList = async () => {
const res = await getListScene();
@@ -32,8 +41,10 @@ const getTableList = async () => {
getMarketInfo(marketList.value[0]);
}
};
const getMarketInfo = async (item) => {
const getMarketInfo = async (item: string | number, title?: string) => {
marketIndex.value = item as number;
const data = marketList.value.filter((market, index) => item === index)[0];
if (data) {
const params = {
page: 1,
@@ -43,10 +54,14 @@ const getMarketInfo = async (item) => {
scene_code_info: data.code,
sort: 'quote_nums, desc'
};
// 获取相应的 Info 信息
const res = await getSurveyTemplates(params);
if (res.data.code === 0) {
marketInfo.value = res.data.data;
}
// 获取对应的 itme 信息
marketItem.value = marketList.value.find((item) => item.title === title);
}
};
@@ -70,10 +85,10 @@ onMounted(() => {
}
.market {
margin-top: 10px;
padding: 10px 12px 0;
border-radius: 16px;
background: #fff;
padding-top: 10px;
border-radius: 0 0 16px 16px;
background-color: white;
overflow: hidden;
.market_title {
margin-bottom: 5px;
@@ -82,10 +97,6 @@ onMounted(() => {
}
& > :first-child {
display: flex;
align-items: end;
justify-content: space-between;
span {
color: grey;
font-size: 10px;

View File

@@ -28,11 +28,10 @@ const show = ref(false);
<style scoped lang="scss">
.new_survey {
position: fixed;
bottom: 0;
bottom: 50px;
left: 0;
box-sizing: border-box;
width: 100vw;
padding: 12px 11px 28px;
border-radius: 12px;
background: #fff;
}

View File

@@ -17,140 +17,24 @@
finished-text="没有更多了"
@load="onLoad"
>
<div v-for="item in survey" :key="item" class="new-survey_item">
<!-- 问卷详情 -->
<div class="survey_item_info">
<div style="position: relative">
<div class="survey_item_info_title">
<el-text>
<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="|">
<!--报名签到-->
<div class="flex align-center">
<img
:src="setImg(item.scene_code_info)"
alt="Content Icon"
style="width: 15px; height: 15px"
/>
<el-text size="small">{{ item.owner }}</el-text>
</div>
<!-- 问卷来源 -->
<div class="flex align-center">
<img v-if="item.source === 1" src="../../assets/img/publish/phone.png" alt="" />
<img v-else src="../../assets/img/publish/pc.png" alt="" />
<el-text size="small">{{ item.source === 1 ? '移动端' : 'PC端' }}</el-text>
</div>
<!-- 问卷时间 -->
<div class="flex align-center">
<img src="../../assets/img/publish/time.png" alt="" />
<el-text size="small">{{ item.created_at }}</el-text>
</div>
</el-space>
</div>
<div class="survey_item_status">
<img v-if="item.status === 0" src="../../assets/img/publish/edit.png" alt="" />
<img
v-else-if="item.status === 1"
src="../../assets/img/publish/publish.png"
alt=""
/>
<img v-else-if="item.status === 2" src="../../assets/img/publish/end.png" alt="" />
<!-- <span class="survey_item_info_status_text">-{{ item.status_txt }}-</span>-->
</div>
</div>
<!--问卷描述-->
<div v-if="item.introduction">
<!--<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 功能位置 -->
<div class="survey_item_action">
<!-- <el-space direction="horizontal">-->
<div>
<!-- <el-button :disabled="item.source === 0" @click="deleteItem(item)"> 删除</el-button>-->
<el-button :disabled="item.source === 0" @click="editItem(item)">编辑</el-button>
<el-button style="border: 1px solid #71b73c" @click="toPreview(item)">
<el-text style="color: #71b73c">预览</el-text>
</el-button>
<el-button color="#6fb937" @click="toPublish(item)">
<el-text style="color: white">
{{ item.status === 1 ? '结束投放' : '开启投放' }}
</el-text>
</el-button>
</div>
<el-dropdown
v-if="item.source === 1"
placement="top-end"
trigger="click"
active-color="#71b73c"
>
<!-- <Io5EllipsisHorizontalSharp />-->
<van-icon class-prefix="mobilefont" name="gengduo" size="0.7rem"></van-icon>
<template #dropdown>
<el-dropdown-menu
active-color="#71b73c"
:close-on-click-overlay="false"
:close-on-click-outside="false"
>
<el-dropdown-item @click="copyItem(item)">复制</el-dropdown-item>
<el-dropdown-item @click="deleteItem(item)">删除</el-dropdown-item>
<el-dropdown-item @click="saveTemplate(item)">保存为模板</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- </el-space>-->
</div>
<div v-if="survey.length> 0" v-for="item in survey" :key="item" class="new-survey_item">
<survey-item :survey="item" />
</div>
<empty-container v-else />
</van-list>
</div>
<NewSurvey></NewSurvey>
<NewSurvey v-if="survey.length > 0" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getSurveysPage, copySurveys, deleteSurveys, saveTemplates } from '@/api/home/index.js';
import { finish } from '@/api/survey/index.js';
import { showDialog, showConfirmDialog, showFailToast, showSuccessToast, showToast } from 'vant';
import { useRouter } from 'vue-router';
import NewSurvey from '@/views/Home/components/NewSurvey/index.vue';
import png11 from '@/assets/img/home/11.png';
import png13 from '@/assets/img/home/13.png';
import png14 from '@/assets/img/home/14.png';
import png15 from '@/assets/img/home/15.png';
import png16 from '@/assets/img/home/16.png';
import png17 from '@/assets/img/home/17.png';
import png18 from '@/assets/img/home/18.png';
import png31 from '@/assets/img/home/31.png';
const router = useRouter();
const survey = ref([]);
const total = ref(0);
const loading = ref(false);
const finished = ref(false);
const form = ref({
page: 0,
pageSize: 10,
project_name: ''
});
import { onMounted } from 'vue';
const searchValue = ref('');
import NewSurvey from '@/views/Home/components/NewSurvey/index.vue';
import EmptyContainer from '@/views/Survey/components/EmptyContainer.vue';
import SurveyItem from '@/views/Survey/components/SuvreyItem.vue';
import { form, fetchSurveys, loading, finished, survey, searchValue } from '@/views/Survey/hooks/useSurveyData';
const blurs = () => {
form.value.page = 1;
@@ -165,161 +49,15 @@ const onLoad = () => {
fetchSurveys();
}, 500);
};
const fetchSurveys = async () => {
const params = {
page: form.value.page,
per_page: form.value.pageSize,
group_id: 0,
project_name: searchValue.value
};
const res = await getSurveysPage(params);
if (res.data.code === 0) {
survey.value = survey.value.concat(res.data.data);
total.value = res.data.meta.total;
survey.value.forEach((item) => {
const sceneName = JSON.parse(JSON.stringify(item.scene_name));
const nameList = sceneName.split('-');
if (nameList.length > 0) {
item.scene_name = nameList[1] ? nameList[1] : nameList[0];
}
const timeList = item.created_at.split(' ');
if (nameList.length) {
item.created_at = timeList[0];
}
});
loading.value = false;
// 数据全部加载完成
if (survey.value.length >= total.value) {
finished.value = true;
}
} else {
// Toast()
}
};
const deleteItem = (item) => {
showDialog({
title: `确认删除问卷${item.project_name} ?`,
showCancelButton: true,
confirmButtonColor: '#03B03C'
})
.then(async () => {
const res = await deleteSurveys(item.sn);
if (res.data.message) {
showToast(res.data.message);
} else {
showToast('删除成功!');
}
form.value.page = 1;
survey.value = [];
await fetchSurveys();
})
.catch(() => {
if (res.data.message) {
showToast(res.data.message);
}
});
};
const copyItem = (item) => {
showDialog({
title: `确认复制问卷${item.project_name} ?`,
showCancelButton: true,
confirmButtonColor: '#03B03C'
})
.then(async () => {
const res = await copySurveys(item.sn);
if (res.data.code === 200 || res.data.code === 201) {
showSuccessToast('复制成功');
form.value.page = 1;
survey.value = [];
await fetchSurveys();
} else {
showFailToast(res.data);
}
})
.catch(() => {
// on cancel
});
};
const toPreview = (item) => {
router.push({
path: '/preview',
query: {
sn: item.sn,
name: item.project_name,
source: 0
}
});
};
const toPublish = (item) => {
if (item.status === 1) {
showDialog({
title: `确定要取消投放${item.project_name}?`,
showCancelButton: true
})
.then(() => {
finish(item.sn).then((res) => {
if (res.data) {
// 把数据改掉
item.status = 2;
}
});
})
.catch(() => {
// on cancel
});
} else {
router.push({
path: '/publish',
query: {
sn: item.sn
}
});
}
};
const editItem = (item) => {
router.push({
path: '/create',
query: {
sn: item.sn
}
});
};
// 保存为模板
const saveTemplate = async (item) => {
const data = JSON.parse(JSON.stringify(item));
const res = await saveTemplates(item.sn, data);
if (res.data.code === 200 || res.data.code === 201) {
showConfirmDialog({
message: '模板保存成功,请前往模板市场查看!',
showCancelButton: false
});
} else {
showFailToast(res.data);
}
};
const setImg = (code) => {
const imageMap = {
11: png11,
13: png13,
14: png14,
15: png15,
16: png16,
17: png17,
18: png18,
31: png31
};
// 默认返回 png11 如果 code 不存在
return imageMap[code] || png11;
};
onMounted(() => {
// fetchSurveys();
});
</script>
<style scoped lang="scss">
@import '@/assets/css/base';
@import '@/assets/css/main';
@import "@/assets/css/base";
@import "@/assets/css/main";
.el-dropdown-menu__item:not(.is-disabled):focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
@@ -336,125 +74,12 @@ onMounted(() => {
}
.new-survey-container {
//min-height: calc(100vh - 100px);
//padding: 1px;
//background: linear-gradient(to bottom, $theme-color 200px, #f2f2f2 300px);
margin-top: 10px;
.new-survey_item {
margin: 0 10px 10px;
padding: 10px 0 8px 7px;
border-radius: 8px;
background-color: white;
.survey_item_info {
.survey_item_info_status {
display: flex;
margin-bottom: 15px;
img {
height: 12px;
margin-right: 3px;
}
}
.survey_item_status {
position: absolute;
top: -16px;
right: -10px;
.survey_item_info_status_text {
height: 12px;
color: rgba(206, 206, 206, 1);
//font-family: TBMCYXT;
font-weight: normal;
font-size: 11px;
line-height: 13px;
letter-spacing: 1.0083px;
text-align: left;
white-space: nowrap;
overflow-wrap: break-word;
}
}
.survey_item_info_title {
display: flex;
.el-text {
font-size: 15px;
// 添加以下样式处理文本溢出
&:first-child {
display: inline-block;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
& > :nth-child(2) {
position: relative;
left: 10px;
padding: 1px 3px;
border: 2px solid #f5f5f5;
border-radius: 6px;
font-size: 12px;
}
}
.survey_item_info_desc {
//padding: 7px 9px 6px;
border-radius: 8px;
//background-color: rgba(246, 247, 248, 0.5);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
box-sizing: border-box;
margin: 0 10px 10px 10px;
//padding: 8px;
//border-radius: 8px;
//background: #f6f7f8;
color: #828282;
font-weight: 400;
font-style: normal;
font-size: 12px;
.el-text {
width: 323px;
height: 32px;
color: rgba(130, 130, 130, 1);
//font-family: PingFangSC-Regular;
font-weight: normal;
font-size: 12px;
line-height: 16px;
text-align: left;
overflow-wrap: break-word;
}
}
}
.survey_item_action {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px;
border-top: 1px dashed #f5f5f5;
.el-button {
width: 18vw;
border-radius: 8px;
}
.el-space {
display: flex;
justify-content: space-evenly;
}
}
}
.new-survey_item + .new-survey_item {

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
</script>
<template>
<el-empty>
<template #image>
</template>
<template #description>
<el-text> - 更多任务期待您的创建 - </el-text>
</template>
<el-button type="primary" color="#5cca07">新建问卷</el-button>
</el-empty>
</template>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,339 @@
<script setup lang="ts">
import png11 from '@/assets/img/home/11.png';
import png13 from '@/assets/img/home/13.png';
import png14 from '@/assets/img/home/14.png';
import png15 from '@/assets/img/home/15.png';
import png16 from '@/assets/img/home/16.png';
import png17 from '@/assets/img/home/17.png';
import png18 from '@/assets/img/home/18.png';
import png31 from '@/assets/img/home/31.png';
import { showDialog, showFailToast, showSuccessToast } from 'vant';
import { finish } from '@/api/survey';
import { copySurveys } from '@/api/home';
import { useRouter } from 'vue-router';
import { form, fetchSurveys, deleteItem, saveTemplate } from '@/views/Survey/hooks/useSurveyData';
// router
const router = useRouter();
const survey = defineModel<SurveyItem>('survey', { required: true });
function setImg(code: number) {
const imageMap: { [key: number]: string } = {
11: png11,
13: png13,
14: png14,
15: png15,
16: png16,
17: png17,
18: png18,
31: png31
};
// 默认返回 png11 如果 code 不存在
return imageMap[code] || png11;
};
function editItem(item) {
router.push({
path: '/create',
query: {
sn: item.sn
}
});
};
function toPreview(item) {
router.push({
path: '/preview',
query: {
sn: item.sn,
name: item.project_name,
source: 0
}
});
};
function toPublish(item) {
if (item.status === 1) {
showDialog({
title: `确定要取消投放${item.project_name}?`,
showCancelButton: true
})
.then(() => {
finish(item.sn).then((res) => {
if (res.data) {
// 把数据改掉
item.status = 2;
}
});
})
.catch(() => {
// on cancel
});
} else {
router.push({
path: '/publish',
query: {
sn: item.sn
}
});
}
};
function copyItem(item) {
showDialog({
title: `确认复制问卷${item.project_name} ?`,
showCancelButton: true,
confirmButtonColor: '#03B03C'
})
.then(async () => {
const res = await copySurveys(item.sn);
if (res.data.code === 200 || res.data.code === 201) {
showSuccessToast('复制成功');
form.value.page = 1;
survey.value = [];
await fetchSurveys();
} else {
showFailToast(res.data);
}
})
.catch(() => {
// on cancel
});
};
</script>
<template>
<section>
<!-- 问卷详情 -->
<div class="survey_item_info">
<div style="position: relative">
<div class="survey_item_info_title">
<el-text>
<b v-html="survey.project_name"></b>
</el-text>
<el-text>{{ survey.answer_num }}</el-text>
<el-text v-if="survey.is_time" size="small">
{{ `${survey.start_time} ${survey.end_time ?? '无限期'}` }}
</el-text>
</div>
<div class="survey_item_info_status">
<el-space spacer="|">
<!--报名签到-->
<div class="flex align-center">
<img
:src="setImg(survey.scene_code_info)"
alt="Content Icon"
style="width: 15px; height: 15px"
/>
<el-text size="small">{{ survey.owner }}</el-text>
</div>
<!-- 问卷来源 -->
<div class="flex align-center">
<img
v-if="survey.source === 1"
src="../../assets/img/publish/phone.png"
alt=""
/>
<img v-else src="../../assets/img/publish/pc.png" alt="" />
<el-text size="small">
{{ survey.source === 1 ? '移动端' : 'PC端' }}
</el-text>
</div>
<!-- 问卷时间 -->
<div class="flex align-center">
<img src="../../assets/img/publish/time.png" alt="" />
<el-text size="small">{{ survey.created_at }}</el-text>
</div>
</el-space>
</div>
<div class="survey_item_status">
<img
v-if="survey.status === 0"
src="../../assets/img/publish/edit.png"
alt=""
/>
<img
v-else-if="survey.status === 1"
src="../../assets/img/publish/publish.png"
alt=""
/>
<img
v-else-if="survey.status === 2"
src="../../assets/img/publish/end.png"
alt=""
/>
</div>
</div>
<!--问卷描述-->
<el-space class="survey_item_info_desc" spacer="|">
<section>
<el-text size="small">回收数量</el-text>
<el-text> {{ survey.answer_num }}</el-text>
</section>
<section>
<el-text size="small">回收数量进度</el-text>
<el-text>{{ survey.recycle_progress }}</el-text>
</section>
<section>
<el-text size="small">投放时间进度</el-text>
<el-text> {{ survey.recycle_time }}</el-text>
</section>
</el-space>
</div>
<!-- action 功能位置 -->
<div class="survey_item_action">
<div>
<el-button :disabled="survey.source === 0" @click="editItem(survey)">
编辑
</el-button>
<!-- <el-button-->
<!-- style="border: 1px solid #71b73c"-->
<!-- @click="toPreview(item)"-->
<!-- >-->
<!-- <el-text style="color: #71b73c">复制</el-text>-->
<!-- </el-button>-->
<el-button
style="border: 1px solid #71b73c"
@click="toPreview(survey)"
>
<el-text style="color: #71b73c">预览</el-text>
</el-button>
<el-button color="#6fb937" @click="toPublish(survey)">
<el-text style="color: white">
{{ survey.status === 1 ? '结束投放' : '开启投放' }}
</el-text>
</el-button>
</div>
<el-dropdown
v-if="survey.source === 1"
placement="top-end"
trigger="click"
active-color="#71b73c"
>
<van-icon
class-prefix="mobilefont"
name="gengduo"
size="0.7rem"
></van-icon>
<template #dropdown>
<el-dropdown-menu
active-color="#71b73c"
:close-on-click-overlay="false"
:close-on-click-outside="false"
>
<el-dropdown-item @click="copyItem(survey)">
复制
</el-dropdown-item>
<el-dropdown-item @click="deleteItem(survey)">
删除
</el-dropdown-item>
<el-dropdown-item @click="saveTemplate(survey)">
保存为模板
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</section>
</template>
<style scoped lang="scss">
@import "@/assets/css/base";
@import "@/assets/css/main";
.survey_item_info {
.survey_item_info_status {
display: flex;
margin-bottom: 15px;
img {
height: 12px;
margin-right: 3px;
}
}
.survey_item_status {
position: absolute;
top: -16px;
right: -10px;
}
.survey_item_info_title {
display: flex;
.el-text {
font-size: 15px;
&:first-child {
display: inline-block;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
& > :nth-child(2) {
position: relative;
left: 10px;
padding: 1px 3px;
border: 2px solid #f5f5f5;
border-radius: 6px;
font-size: 12px;
}
}
.survey_item_info_desc {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: space-around;
border-radius: 8px;
background-color: #f6f7f8;
overflow: hidden;
text-overflow: ellipsis;
padding: 10px 15px;
box-sizing: border-box;
margin: 0 10px 10px 10px;
color: #828282;
font-weight: 400;
font-size: 12px;
section {
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
& > :nth-last-child(-n+1) {
font-size: 20px;
font-weight: bold;
color: $theme-color;
}
}
}
}
.survey_item_action {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px;
border-top: 1px dashed #f5f5f5;
.el-button {
width: 18vw;
border-radius: 8px;
}
.el-space {
display: flex;
justify-content: space-evenly;
}
}
</style>

View File

@@ -0,0 +1,95 @@
import {
getSurveysPage, deleteSurveys,
saveTemplates
} from '@/api/home';
import { ref } from 'vue';
import {
showDialog,
showConfirmDialog,
showFailToast,
showToast
} from 'vant';
const form = ref({
page: 0,
pageSize: 10,
project_name: ''
});
const searchValue = ref('');
const survey = ref<SurveyItem[]>([]);
const total = ref(0);
const loading = ref(false);
const finished = ref(false);
async function fetchSurveys() {
const params = {
page: form.value.page,
per_page: form.value.pageSize,
group_id: 0,
project_name: searchValue.value
};
const res = await getSurveysPage(params);
if (res.data.code === 0) {
survey.value = survey.value.concat(res.data.data);
total.value = res.data.meta.total;
survey.value.forEach((item) => {
const sceneName = JSON.parse(JSON.stringify(item.scene_name));
const nameList = sceneName.split('-');
if (nameList.length > 0) {
item.scene_name = nameList[1] ? nameList[1] : nameList[0];
}
const timeList = item.created_at.split(' ');
if (nameList.length) {
item.created_at = timeList[0];
}
});
loading.value = false;
// 数据全部加载完成
if (survey.value.length >= total.value) {
finished.value = true;
}
} else {
// Toast()
}
};
function deleteItem(item) {
showDialog({
title: `确认删除问卷${item.project_name} ?`,
showCancelButton: true,
confirmButtonColor: '#03B03C'
})
.then(async () => {
const res = await deleteSurveys(item.sn);
if (res.data.message) {
showToast(res.data.message);
} else {
showToast('删除成功!');
}
form.value.page = 1;
survey.value = [];
await fetchSurveys();
})
.catch(() => {
// on cancel
});
};
// 保存为模板
async function saveTemplate(item) {
const data = JSON.parse(JSON.stringify(item));
const res = await saveTemplates(item.sn, data);
if (res.data.code === 200 || res.data.code === 201) {
showConfirmDialog({
message: '模板保存成功,请前往模板市场查看!',
showCancelButton: false
});
} else {
showFailToast(res.data);
}
}
;
export { form, fetchSurveys, loading, finished, survey, total, searchValue, deleteItem, saveTemplate };

18
src/views/Survey/types/survey.d.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
type SurveyItem = {
scene_name: string
project_name: string
created_at: string
created_user?: string
scene_code_info?: number
owner?: string
source?: number
status?: number
answer_num?: number | string
recycle_progress?: string
recycle_time?: string
sn: string
is_time?: boolean
start_time?: string
end_time?: string | null
status_txt?: string
}