133 Commits

Author SHA1 Message Date
Huangzhe
b5642d0d3d feat: 新增神策数据埋点和广告页面功能 2025-06-24 22:43:31 +08:00
黄泽-huangze-天津易商数智
acc02c5a75 refactor(sensors): 重构神策数据采集逻辑
- 移除了全局的 pageStayTime 变量和相关的页面停留时间采集逻辑
- 在 AD 和 Home 组件中添加了具体的神策数据采集函数
- 优化了 banner 点击事件的数据采集
- 删除了不必要的神策数据插件配置
2025-06-18 15:15:07 +08:00
黄泽-huangze-天津易商数智
ec278a03b7 feat: 添加神策数据统计SDK及相关插件 2025-06-10 17:39:53 +08:00
黄泽-huangze-天津易商数智
49cbb071e8 feat(analytics): 优化神策数据插件配置并添加点击跟踪指令
- 修改 usePieChart 钩子,注释掉 console.log 语句
- 更新 sensorsData 插件配置,关闭单页面自动跟踪和滚动热图
- 新增自定义指令 saTrack 用于手动跟踪点击事件
- 在 ImageSlider 组件中应用 saTrack 指令,跟踪轮播图点击
2025-06-09 13:57:32 +08:00
黄泽-huangze-天津易商数智
f9775f4a57 feat: 为页面动态取标题 2025-06-05 16:01:09 +08:00
Huangzhe
281fee561c feat(main): 集成神策数据插件
- 在 main.ts 中引入并注册神策数据插件
- 新增 sa.ts 文件实现神策数据插件功能
- 初始化神策数据并配置相关选项
- 在全局属性中添加 $sensorsData
2025-06-04 15:23:11 +08:00
陈昱达
a62039d851 style(layout): 调整后退图标大小
- 在 index.vue 和 redirect.vue 文件中,将 .back-icon 类的宽度和高度从 18px 调整为 24px
-此修改统一了后退图标的尺寸,提高了用户界面的一致性
2025-06-03 17:25:27 +08:00
陈昱达
6a09aef7a7 feat(layout): 替换导航栏后退图标为自定义图片- 在 index.vue 和 redirect.vue 中将后退图标改为自定义图片
- 保留原有代码并添加注释,以备未来可能的更改
2025-06-03 16:56:00 +08:00
Huangzhe
73e1c89f5d Merge branch 'release-v1.0.0' into feature/feature-20250430-h5 2025-05-29 19:00:03 +08:00
Huangzhe
9462843e6a fix: correct build_prod script command by adding missing build parameter 2025-05-29 18:11:36 +08:00
Huangzhe
7502c2c800 fix: correct build:prod script name to build_prod in package.json 2025-05-29 18:05:44 +08:00
Huangzhe
520197e9b8 Merge branch 'uat' into feature/feature-20250430-h5 2025-05-29 18:04:28 +08:00
Huangzhe
a6adf8dab7 fix: correct build_prod script command in package.json 2025-05-29 18:04:23 +08:00
Huangzhe
2d28c9efcb feat: add CreateSurvey component with template selection and AI generation options 2025-05-29 16:51:25 +08:00
Huangzhe
4982e283a8 chore (text): 更改 主页描述信息 2025-05-29 15:19:06 +08:00
Huangzhe
0173b0b05e feat: 添加条形统计图模板 2025-05-29 15:15:42 +08:00
Huangzhe
b1fb42668c Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-29 15:13:51 +08:00
陈昱达
dc63cec92a feat: 修改内容推荐标题为创新速递
- 在 HomeRecommend组件中,将推荐内容的标题从"内容推荐"修改为"创新速递"
- 此修改旨在更好地反映推荐内容的性质,提升用户体验
2025-05-29 15:12:35 +08:00
陈昱达
8f06726bb6 refactor(analysis): 优化分析组件和移除冗余日志输出
- 移除了 Analysis组件中的多余 console.log 语句
- 优化了MineTask组件中的轮播图高度计算逻辑- 删除了 useAnalysis hook 中的冗余日志输出
2025-05-29 12:08:14 +08:00
Huangzhe
af323f2c74 feat(env): 添加 AI Agent URL 配置并支持环境变量
- 在 .env.prod 和 .env.uat 文件中添加 VITE_APP_AIAGENTURL 变量
- 修改 IntelligentGeneration 组件,使用环境变量替代硬编码的 URL
2025-05-29 10:56:25 +08:00
Huangzhe
e89f554830 fix: 修复 echart 无法正确渲染的问题 2025-05-28 23:33:39 +08:00
陈昱达
966e56ad74 refactor(样式): 优化输入框样式和矩阵文本组件
- 调整了 main.scss 和 public.scss 中的样式
-重构了 MatrixText 组件,支持数字和文本输入
- 优化了 Completion 组件的默认值设置
- 修改了验证逻辑,允许空值作为有效答案
2025-05-28 23:19:14 +08:00
陈昱达
f4f84e0b68 feat(Design): 完善填空题输入类型并添加验证
- 新增数字和小数填空类型
- 添加输入限制和验证功能
- 优化文本类型填空的验证逻辑
- 修复了一些与填空题相关的小问题
2025-05-28 21:09:01 +08:00
Huangzhe
e3269c555d feat(Market): 优化模板市场组件的搜索结果展示
- 在无符合搜索结果时显示"无符合要求结果"提示
-保留原有的"更多模板期待您的探索"文案
2025-05-28 20:15:11 +08:00
Huangzhe
880c67b451 Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-28 20:06:59 +08:00
Huangzhe
1de9b5b539 feat(Home): 优化模板市场功能
- 移除未使用的 imgMap 导入
- 添加市场列表数据存储到 sessionStorage
- 优化模板数据请求逻辑
- 修复搜索功能和分页问题
- 调整 throttle 函数调用
2025-05-28 20:06:32 +08:00
陈昱达
4521f0b443 feat(Survey): 增加循环页面有效性判断
- 在原有的 surveyValid 变量基础上,添加 cycleValid 变量来判断循环页面是否有效
- 将 cycleValid 添加到返回的有效性判断条件中,确保循环页面也参与整体有效性检查
2025-05-28 19:31:42 +08:00
Huangzhe
4a6d277b32 fix: 修复预览异常的问题 2025-05-28 19:22:36 +08:00
Huangzhe
ba0dae323a refactor(Home): 修改市场组件每页显示数量
- 将 Market 组件中每页显示数量从 10 条修改为 100 条
2025-05-28 18:30:38 +08:00
Huangzhe
fedfab75be refactor(Home): 优化市场模板搜索功能
- 移除了搜索值为空时重置索引的逻辑
- 在点击搜索时重置当前参数数据,包括关键字和索引
2025-05-28 18:26:28 +08:00
Huangzhe
7c5b9e460a refactor(Home): 优化市场调研模板搜索功能
- 引入 useSurveySearch 钩子以支持调查模板搜索
- 在搜索值变化时重置索引到 1
2025-05-28 18:22:08 +08:00
陈昱达
3fa7b6e2e1 refactor(Survey): 优化问卷数据验证逻辑
- 修改逻辑验证条件,提高代码可读性- 增加对随机题组和循环题组的判断- 优化验证流程,提高整体问卷数据质量
2025-05-28 18:10:19 +08:00
Huangzhe
6f11b91849 style(survey): 修改 AI 洞察背景样式
- 移除 AI 洞察卡片的图片背景
- 添加从浅蓝色到白色的线性渐变背景
2025-05-28 17:40:16 +08:00
Huangzhe
5ab49b7b66 ci: 添加生产环境配置并更新构建脚本
- 新增 .env.prod 文件,包含生产环境的 Vite 配置
- 在 package.json 中添加生产环境构建脚本
2025-05-28 17:25:28 +08:00
Huangzhe
5bcc6db202 feat(utils): 新增节流函数并优化市场组件加载逻辑
- 在 utils.js 中添加了 throttle 函数,用于节流操作
- 在 Market 组件中实现了无限滚动加载功能
- 优化了市场信息获取逻辑,增加了分页处理
- 更新了 yl.png 文件
2025-05-28 17:23:30 +08:00
Huangzhe
1181dc0f52 fix: 修复主页数据异常 2025-05-28 17:21:50 +08:00
Huangzhe
0084ed80d0 fix: 修复问卷完成的时候多次展示 2025-05-28 16:22:12 +08:00
Huangzhe
7822ce4516 feat(share): 添加微信分享功能并优化相关配置
- 新增 getWXShareConfig 函数用于获取微信分享配置
- 在 router 中集成分享功能,使用 getWXShareConfig 替代硬编码的分享信息
- 添加 setWXShareConfig 函数,用于设置分享配置,包括标题、描述、缩略图等
- 在 AD 页面中添加动态修改 document.title 的逻辑
- 优化 SurveyItem 组件中的标题显示逻辑,根据标题宽度动态调整样式
2025-05-28 16:07:47 +08:00
陈昱达
92a28bca1e Merge remote-tracking branch 'origin/feature/feature-20250430-h5' into feature/feature-20250430-h5 2025-05-28 15:09:22 +08:00
陈昱达
8561011f9b refactor(Design): 为 MatrixText 组件添加占位符并优化代码格式
- 在 MatrixText 组件的输入框中添加占位符
- 格式化 validate 相关的测试代码,使其更加规范
- 在 validateMatrixCheckbox 中添加日志输出,便于调试
- 优化 language 相关代码,新增请求数量相关的翻译文本
2025-05-28 15:09:04 +08:00
Huangzhe
88da1cd002 feat(router): 修改分享链接
- 在分享功能中,将当前页面
2025-05-28 15:00:43 +08:00
Huangzhe
12b25d8148 Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-28 14:57:38 +08:00
Huangzhe
8415818f28 style(AD): 优化广告页面样式和布局
- 调整按钮显示逻辑,增加对 hasShare 变量的判断
- 修改按钮样式和位置,使其适应不同设备和场景
2025-05-28 14:57:26 +08:00
陈昱达
9afd82d9d7 fix(Survey): 修复预览页面选项数限制逻辑
-增加对最小选择数和最大选择数的非空判断- 添加日志输出,便于调试
2025-05-28 14:35:51 +08:00
Huangzhe
05646a31ae feat(router): 添加分享页面并优化广告页面逻辑
- 在路由中添加 share 页面,用于展示分享内容
- 在 AD 页面中增加 hasShare 状态,用于判断是否处于分享页面
- 根据 hasShare 状态动态调整 AD 页面样式
- 新增 Share 组件,用于渲染分享页面
2025-05-28 14:33:00 +08:00
Huangzhe
a121e2ce81 fix: 修复表格在 ios 异常显示的问题 2025-05-28 14:06:37 +08:00
Huangzhe
544673c350 feat: 增加最大搜索限制 2025-05-28 13:48:00 +08:00
Huangzhe
09ab4901d8 feat(search): 优化搜索功能并调整搜索事件处理
- 将 Search 组件的 @change 事件改为 @search 事件
- 实现搜索框无值时清空 keyword 并重新加载列表的功能
- 优化 fetchTemplate 方法,使用 keyword 替代 searchValue.value
2025-05-28 13:30:02 +08:00
Huangzhe
709f56ac74 Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-28 13:06:47 +08:00
Huangzhe
93bb2b3594 refactor(layout): 优化市场指数和调查组件布局
- 在 Market/Index.vue 中添加搜索值变量
- 在 SurveyItem.vue 中优化调查标题样式计算逻辑
2025-05-28 13:06:33 +08:00
陈昱达
e543896c2a refactor(Design): 优化文件上传和答案处理逻辑
- 修改 FileUpload 组件,将答案直接赋值给 question.value.answer
- 更新 useFileUploadHooks,使用 FileList | [] 类型
-移除 PreviewFileUpload 组件中的无用代码
- 优化 PreviewTextWithImages 组件的 watch监听
- 调整 Survey 预览中的表单验证逻辑
2025-05-28 12:33:21 +08:00
Huangzhe
fced5acf2f update dockerfile 2025-05-27 22:24:46 +08:00
Huangzhe
1549dc704d update dockerfile 2025-05-27 22:22:35 +08:00
Huangzhe
d49f856fd0 feat(docker): 更新 Nginx 配置并注释 Dockerfile
- 在 default.conf 中添加了访问日志和错误日志配置
- 新增了日志文件访问接口
- 注释掉了 Dockerfile 中的配置文件复制指令
2025-05-27 21:58:20 +08:00
Huangzhe
73841a2d07 Merge branch 'uat' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-27 21:47:13 +08:00
陈昱达
f6be0143bc build: 更新 Dockerfile 配置
- 替换自定义 nginx 配置文件为默认配置文件
- 调整 URL目录设置
- 移除多余的 nginx.conf 文件
2025-05-27 21:46:07 +08:00
Huangzhe
5b79337c39 Merge branch 'feature/feature-20250430-h5' into uat 2025-05-27 21:35:38 +08:00
Huangzhe
4d13a28eb2 refactor/nginx: 重构 NGINX 配置文件
-移除了不必要的全局配置,简化了配置文件结构
- 删除了未使用的日志格式和访问日志配置
- 移除了反向代理和日志文件访问相关配置
- 保留了基本的服务器配置和错误页面配置
2025-05-27 21:34:16 +08:00
Huangzhe
e39982c6f5 update nginx 2025-05-27 21:16:54 +08:00
陈昱达
13d30a51a8 config(vite): 更新 Vite配置以支持特定主机
- 在 Vite 配置中添加 allowedHosts 选项,允许 yiligpt.x.digitalyili.com 主机
- 优化代码格式,调整导入语句的分号
2025-05-27 21:16:02 +08:00
Huangzhe
94330b1f01 feat(router): 优化模板预览页面的头部样式
- 为模板预览页面添加绿色背景的头部
- 调整搜索页面模板标题的显示逻辑
- 优化 AD 页面按钮的点击事件
- 移除 Analysis 页面的 nextTick 包装
2025-05-27 20:49:07 +08:00
陈昱达
44bf17d47b feat(docker): 添加自定义 nginx 配置并调整反向代理设置
- 添加自定义 nginx配置文件,用于反向代理到伊利 GPT
- 修改 Dockerfile,使用新的 nginx 配置
- 更新前端代码,将主机地址改为当前窗口地址
2025-05-27 20:47:53 +08:00
Huangzhe
10a4904e26 fix(Survey): 优化移动端问卷模板保存校验
- 更新了错误提示信息,增加了对逻辑设置的提及
- 增加了对问卷逻辑的校验,只允许自动填写类型的逻辑
2025-05-27 19:46:54 +08:00
Huangzhe
079a589510 fix(Analysis): 修复饼图数据展示问题
- 调整饼图数据处理逻辑,确保空数据不被展示
-优化 NPS 和 5 点量表的饼图数据处理方式
- 使用 nextTick 确保图表在数据更新后正确渲染
2025-05-27 18:11:04 +08:00
陈昱达
3f0bc59a0a fix(Survey): 修复预览页面点击下一题时错误提示未清除的问题
- 在 next 函数中添加代码,清除所有问题的错误提示
- 优化用户体验,确保每次翻页时重新开始校验答案
2025-05-27 17:11:38 +08:00
Huangzhe
965f4df493 feat(router): 为预览页面添加头部样式
- 在预览页面的 meta 信息中添加 header 对象
- 设置背景色为 green
- 设置 pureBGC 为 false
2025-05-27 17:04:18 +08:00
陈昱达
7650402cc2 style(components): 优化导航栏布局和首页推荐样式
- 在导航栏组件中添加 nav-content 类,优化内容布局
- 更新首页推荐组件中的颜色方案,使用新的主题颜色
2025-05-27 16:58:07 +08:00
Huangzhe
1322f26167 Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-27 16:40:09 +08:00
Huangzhe
9d478ce108 style(survey): 调整预览页面容器样式
- 移除了 v-memo 指令
- 添加了 preview-container 类的 height 属性
- 调整了 max-height 属性的计算方式
2025-05-27 16:39:51 +08:00
Huangzhe
0f1e1839da style(Survey): 调整标题宽度判断逻辑
- 将标题宽度的判断阈值从 130 修改为 120
2025-05-27 16:33:19 +08:00
陈昱达
55cfd96601 fix(upload): 修复上传组件最小尺寸设置问题
- 将 FileUpload.js 中的 min_size 默认值从 0 改为 1
- 在 FieldUploadQuestionAction.vue 中,将 min_size 的最小值设置为 1
-确保用户在上传文件时至少选择一个文件,避免出现空文件上传的情况
2025-05-27 16:31:06 +08:00
陈昱达
48bab50a7e refactor(components): 优化 YlTable 组件布局
- 重构了 tableWidth 计算逻辑,提高了代码可读性和性能
- 添加了 doc_content 类名,便于样式管理和维护
- 使用可选链操作符简化了代码结构
2025-05-27 16:26:06 +08:00
陈昱达
d9187a0694 style(layout): 调整首页布局和样式
- 调整 HomeRecommend 组件的 more样式,增加上边距- 修正 Home 页面的条件渲染逻辑,确保正确显示我的问卷和推荐内容
2025-05-27 15:52:43 +08:00
陈昱达
b63e336c08 style(css): 优化主题颜色和样式
- 将 primary-color调整为小写
- 添加 el-color-primary 变量- 优化导航栏背景样式- 更新表格组件,增加排序功能
- 改进首页推荐组件,优化数据展示
2025-05-27 15:49:35 +08:00
Huangzhe
6eed4bf53e style: 隐藏市场模块的下划线
- 在 Market/Index.vue 文件中,将 :deep(.van-tabs__line) 的样式从设置背景色改为隐藏
- 使用 !important 确保样式优先级,以覆盖其他样式
2025-05-27 15:45:46 +08:00
Huangzhe
74ad7a19eb style(AnalysisInfo): 调整分析信息样式
- 调整行高为 30px
2025-05-27 15:36:14 +08:00
Huangzhe
c073cd38f2 refactor(layouts): 优化 redirect 页面样式控制逻辑
- 修复 redirectHeaderStyle 计算属性中的条件判断逻辑
- 在路由元信息中调整 pureBGC 属性的使用方式
2025-05-27 15:27:09 +08:00
Huangzhe
1ec224181d Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-27 15:21:27 +08:00
Huangzhe
1ec680ef16 refactor(survey): 优化分析页面图表展示和样式调整
- 调整图表高度计算逻辑,优化长标题的展示效果
- 修复饼图数据为空时的展示问题
- 调整模板市场页面的头部样式
- 优化调查问卷列表的样式
- 移除分析页面的重复代码和无效注释
2025-05-27 15:20:48 +08:00
陈昱达
623ffb13e3 feat(Table): 增加表格列自定义渲染功能并优化首页推荐组件
- 在 YlTable 组件中添加 RenderSlot 组件,实现自定义渲染功能
- 在 HomeRecommend 组件中使用自定义渲染功能,根据数据动态显示颜色
- 优化 HomeRecommend组件的代码结构
2025-05-27 15:03:41 +08:00
Huangzhe
af8b2765bc Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-27 14:47:58 +08:00
Huangzhe
0dd4600d35 feat(survey): 优化问卷列表和分析页面
- 更新了多个图片资源
- 优化了问卷分析页面的图表展示逻辑
- 调整了导航栏和重定向页面的样式
- 统一了空容器组件的样式
- 优化了问卷列表页面的布局
2025-05-27 14:40:00 +08:00
陈昱达
91d1cbc21b refactor(components): 优化多个组件的样式和逻辑
-调整 tooltip 内容显示逻辑,优化样式
- 修复饼图数据为空时的显示问题- 优化表格宽度计算逻辑- 修复饼图初始化问题- 优化问卷分析页面样式
2025-05-27 14:12:56 +08:00
陈昱达
cf0eb9a57c refactor(components): 优化 YlTable 组件的表格宽度计算逻辑
- 移除了原有的 docWidth 判断逻辑
- 当表格列数小于等于 3 时,改为直接使用屏幕宽度减去60 作为表格宽度
- 简化了代码结构,提高了可读性和维护性
2025-05-27 10:55:13 +08:00
Huangzhe
2b297d5e80 refactor(components): 修改 tooltip 展示内容并注释 iframe 相关代码
- 在 Tootips 组件中,将 tooltip 展示内容从 row.logic_text 改为 row.text
- 在 IntelligentGeneration 组件中,注释掉 iframe 相关代码,暂时保留未删除
2025-05-27 10:48:03 +08:00
Huangzhe
1cf2ad8155 feat: add carousel component for displaying user tasks with swipeable slides 2025-05-27 10:09:12 +08:00
陈昱达
a2fbf185a1 refactor(CreateSurvey): 更新智能生成组件的 API 地址
- 移除旧的 API 地址和测试地址
- 添加新的智能生成 API 地址
- 简化代码结构,提高可读性
2025-05-26 21:44:05 +08:00
Huangzhe
e587ed96b2 refactor(Survey): 优化移动端模板保存提示信息
- 修改了模板校验失败时的提示信息,明确指出移动端不支持的题型
- 建议用户在 PC 端编辑后重新保存
2025-05-26 21:16:38 +08:00
陈昱达
d58df5727a Merge remote-tracking branch 'origin/feature/feature-20250430-h5' into feature/feature-20250430-h5 2025-05-26 21:13:05 +08:00
陈昱达
b7c7d6b214 refactor(components/views): 优化表格宽度计算和智能生成组件
- 移除了 yl-table-h.vue 中的多余 console.log 语句
- 优化了 CreateSurvey组件中的 IntelligentGeneration 组件代码- 调整了 API 请求的 URL 构造方式,使用动态主机和路径
- 注释掉了未使用的测试 URL
2025-05-26 21:12:43 +08:00
Huangzhe
96495f268c refactor(Design): 优化模板适配和数据加载
- 在 Preview.vue 中添加获取模板详情功能,用于适配模板数据
- 更新模板标题和介绍的获取方式,提高数据准确性和可维护性
- 移除 Market.vue 中的冗余代码,优化组件加载逻辑
2025-05-26 21:11:17 +08:00
Huangzhe
92410be2e0 Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-26 20:35:06 +08:00
Huangzhe
415d7117c3 refactor(Home): 优化市场组件代码结构和逻辑
- 移除了未使用的 watch 和 onMounted 导入
- 简化了 getTableList 方法,直接赋值给 marketList.value
- 修复了获取市场信息的逻辑,使用 h5Title 替代 title
2025-05-26 20:34:35 +08:00
陈昱达
37c0cff959 feat(components): 新增 YlTableH 组件并优化表格展示- 新增 YlTableH组件,支持横向滑动和图片预览功能
- 修改 IntelligentGeneration 组件中的 API 调用地址
- 更新 AnalysisInfo 组件,使用新的 YlTableH组件替换原有的 YlTable
2025-05-26 20:32:49 +08:00
Huangzhe
e2cc6adc19 feat: 更新图片资源并添加新图片
- 新增市场相关的图片资源:11.png、13.png、15.png、16.png、17.png、18.png
- 更新 imgMap.ts 中的图片引用,从 home 文件夹改为 market 文件夹
2025-05-26 19:06:19 +08:00
Huangzhe
8ecb195fca refactor(Home): 优化创建问卷和问题列表组件
- 修改 CreateSurvey 组件中项目名称的默认值为 "请输入问卷标题"
- 优化 QuestionList 组件中 slideChange 函数的参数类型定义
2025-05-26 18:23:13 +08:00
Huangzhe
450dec6c86 refactor: 更新 import 语句以使用 ES 模块
- 将 'vue/dist/vue' 替换为 'vue' 以使用 ES 模块
- 这个改动可以提高代码的可维护性和加载性能
2025-05-26 18:09:01 +08:00
Huangzhe
09efcc851e Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-26 17:59:44 +08:00
陈昱达
44a1f39b82 refactor(Home): 优化 MineTask 组件中的滑动切换逻辑
- 移除了不必要的 console.log 语句
- 优化了 slideChange 函数,提高了代码可读性和性能
2025-05-26 17:58:17 +08:00
陈昱达
ef854a51f8 feat(MineTask): 添加问卷滑动切换功能
- 在 QuestionList 组件中添加 slideChange 事件监听
- 在 MineTask 组件中实现 slideChange 方法,调整滑动后问卷区域高度
-通过自定义事件和回调函数实现问卷页滑动切换时的高度自适应
2025-05-26 17:57:55 +08:00
Huangzhe
633f3c74c4 refactor(Survey): 优化问卷标题的显示逻辑
- 添加 titleRef 用于获取标题元素的宽度
- 在组件挂载时计算标题是否过长,并存储在 isShortTitle 变量中
- 根据标题长度动态调整 surveyTitleStyle 的宽度
2025-05-26 17:57:09 +08:00
Huangzhe
65c783ff02 refactor(home): 重构首页模板市场组件
- 移除 MarketItem 组件中的 setImg 函数,使用新的 imgMap 工具函数替代
- 优化 redirect.vue 中的样式和结构
- 更新 router 中的 meta 信息,增加 bgc 属性
- 新增 imgMap 工具函数,用于获取场景代码对应的图片
- 改进 Market 组件中的标签页样式和逻辑
2025-05-26 17:38:26 +08:00
陈昱达
03c9154602 feat(YlSwiper): 添加自动高度调整功能
- 在 YlSwiper 组件中添加 autoHeight 属性,使轮播图高度能根据内容自动调整
- 在 MineTask 组件中应用该功能,解决任务列表高度适配问题
- 优化 Index.vue 中的代码,增加对 active 值变化的监听,动态调整轮播图高度
2025-05-26 17:17:06 +08:00
Huangzhe
beb74bb3b0 Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-26 17:15:18 +08:00
Huangzhe
bd922cffd5 refactor(Survey): 优化二维码下载提示信息
- 修改了二维码下载成功后的提示信息,使其更加准确和通顺
- 将"请打系统相册查看"改为"请打开系统相册查看"
2025-05-26 17:15:05 +08:00
Huangzhe
c5e424b256 fix: 修复大模型 iframe 跳转问题
- 添加 iframe 加载完成后处理逻辑
- 尝试覆盖 iframe 内的 window.open 方法
- 使用 defineProperty 备选方案以提高兼容性
2025-05-26 17:14:54 +08:00
陈昱达
e392629356 refactor(Survey): 优化移动端题目查看提示样式
- 更新提示文字内容,增加对更多题型兼容的期待
- 调整文字颜色为深灰色,提高可读性
2025-05-26 16:04:16 +08:00
陈昱达
b938f48c50 refactor(Survey/Analysis):优化题目分析组件
- 添加 parentIndex 属性,用于接收上级组件传递的题目编号
- 优化题目标题显示逻辑,增加编号并考虑上级传递的编号
-调整标签显示逻辑,仅在 questionTypeMap 中存在对应类型时显示
2025-05-26 14:45:58 +08:00
Huangzhe
345c80acbc Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-26 14:33:59 +08:00
Huangzhe
18b4f26554 style(Survey): 调整问卷列表样式
- 修改了 surveyTitleStyle 计算属性中的宽度设置
- 根据 isSurveyTime 和 isPublishNumber 的值确定宽度
2025-05-26 14:33:53 +08:00
陈昱达
8fa1059bd7 refactor(components): 优化表格组件和调查分析组件
- 在 YlTable 组件中添加 show-overflow-tooltip 属性,用于控制列内容的显示
- 在 Survey Analysis 组件中设置 tooltip 为 false,优化表格列内容的展示效果
2025-05-26 14:32:14 +08:00
Huangzhe
a0a03e948e Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-26 14:29:56 +08:00
Huangzhe
468d7de19b refactor(Survey): 优化问卷标题样式
- 添加 surveyTitleStyle 计算属性,根据问卷发布时间和编号动态调整标题宽度
- 使用 CSSProperties 类型确保类型安全
- 引入 vant 的 windowWidth 工具函数
2025-05-26 14:29:42 +08:00
陈昱达
81a1e5b127 Merge remote-tracking branch 'origin/feature/feature-20250430-h5' into feature/feature-20250430-h5 2025-05-26 14:27:03 +08:00
陈昱达
c4c00134e7 refactor(components): 优化表格样式和模板市场点击事件
-移除了 YlTable 组件中的多余样式
- 添加了 TemplateMarket 组件中的模板点击事件处理逻辑
- 优化了 AnalysisInfo组件中的问题展示逻辑,增加了对非移动端展示题型的处理
2025-05-26 14:26:13 +08:00
Huangzhe
f5f757b66f Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-26 14:17:28 +08:00
Huangzhe
f886a51110 fix: 修复智能生成问卷 iframe 适配问题
- 更新 iframe src 属性的拼接方式,使用 encodeURIComponent 对 token 进行编码
- 移除 encodeURI 函数的使用,直接使用拼接好的 url
2025-05-26 14:17:17 +08:00
Huangzhe
0a91df64b5 refactor(Survey): 优化模板校验失败提示信息
- 将"内部存在移动端不支持的
2025-05-26 14:13:24 +08:00
Huangzhe
216b443540 feat(Survey): 添加模板校验功能
- 新增模板校验功能,检查问卷中是否包含移动端不支持的题型
- 在保存模板前进行校验,如果存在不支持的题型则弹出提示窗口
- 优化删除问卷和保存模板的逻辑
2025-05-26 14:09:59 +08:00
陈昱达
712985b643 feat(Survey): 优化 EmptyContainer 组件描述内容的展示
- 在描述部分添加 slot 插槽,以支持自定义内容
- 保留原有 errorMsg 显示逻辑,作为默认内容
2025-05-26 14:03:48 +08:00
Huangzhe
41850e5187 fix(store): 修改模板预览逻辑
- 将 create 路径更改为 templatePreview 路径,以正确处理模板预览请求
- 更新 is_preview 和 is_template 参数的判断逻辑,确保在模板预览时设置正确的值
2025-05-26 13:54:11 +08:00
Huangzhe
1043126c72 fix: 修复智能生成页面 iframe 载入问题
- 将 encodeURIComponent 改为 encodeURI 在 iframe src 属性中
2025-05-26 13:22:41 +08:00
Huangzhe
dec8ec0ec0 fix(CreateSurvey): 修复智能生成问卷 iframe src 编码问题
- 将 escapeHTML 函数替换为 encodeURIComponent 函数
2025-05-26 13:06:07 +08:00
Huangzhe
001279874d feat(questionStore): 适应新场景调整问题获取逻辑
- 根据当前 URL 路径动态设置 is_preview 和 is_template 参数
- 优化 SurveyItem 组件中项目名称的样式
2025-05-26 12:03:19 +08:00
Huangzhe
23b1a46bec feat(AD): 添加轮播图按钮点击逻辑
- 在轮播图按钮上添加点击事件处理函数
- 实现按钮点击时的跳转逻辑:
  - 如果当前广告有配置 URL,则跳转到该 URL
  - 如果没有配置 URL,则跳转到智能生成页面
2025-05-26 11:38:25 +08:00
Huangzhe
f9b5d001e5 style(components): 优化多个组件的样式细节
- 修改 YlSwiper 组件的按钮颜色
- 调整 CreateSurvey 组件的标题和描述样式
- 更新 HomeRecommend 组件的标题和布局
- 调整 MineTask 组件的标题位置
2025-05-26 11:32:16 +08:00
Huangzhe
83aee0d18a style(Home): 优化首页推荐组件的样式
- 调整了更多探索提示语的样式和布局
- 添加了 more 类的样式定义,优化了视觉效果
2025-05-26 11:05:33 +08:00
Huangzhe
76b5235ab5 feat(MineTask): 添加问卷列表拖动功能
- 在 Index.vue 中添加拖动开始和结束的事件处理函数
- 在 QuestionList.vue 中根据拖动状态显示/隐藏分页器和导航按钮
- 新增 useDragEvent 钩子用于管理拖动状态
2025-05-26 11:01:10 +08:00
Huangzhe
364b00f51f style(survey): 优化调查页面 2025-05-26 10:36:11 +08:00
Huangzhe
f83e910159 Merge branch 'feature/feature-20250430-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250430-h5 2025-05-26 10:20:11 +08:00
陈昱达
51676a39b4 refactor(components): 优化表格样式和图表数据处理
-调整表格样式,增加右内边距
- 优化饼图数据处理,移除空数据- 修改表格视图 HTML 样式,使用 flex 布局
2025-05-26 10:19:01 +08:00
Huangzhe
36d3df3e80 refactor(Survey): 更新 AI 洞察等待提示文本
- 修改了 Survey 组件中 Wait 组件的提示文本
2025-05-26 10:17:43 +08:00
Huangzhe
2ee26e3e5b style: 修改市场组件背景颜色- 将市场组件的背景颜色从红色改为白色 (#fff) 2025-05-26 10:08:59 +08:00
87 changed files with 2097 additions and 783 deletions

18
.env.prod Normal file
View File

@@ -0,0 +1,18 @@
# .env.development
VITE_APP_BASEURL=https://yls.xapi.digitalyili.com/
VITE_APP_BASEDOMAIM=https://ylst-h5-uat.dctest.digitalyili.com
# VITE_APP_BASEURL=http://192.168.8.165:15011
VITE_APP_ENV=production
VITE_APP_CURRENTMODE=prod
VITE_APP_BASEOSS=https://cxp-pubcos.yili.com/prod-yls
VITE_APP_DELIVERY_BASEURL=https://ylsdist-net.x.digitalyili.com/
VITE_APP_MESSAGE_CENTER=http://gtech-gateway.cxpin.digitalyili.com/apigtech/message-send-center/
VITE_APP_SOCKETURL=wss://yls.xapi.digitalyili.com/survey_sync
VITE_APP_JSONPURL=https://iam.digitalyili.com/idp/restful/getIDPToken
VITE_APP_YQRURL=https://ocp.digitalyili.com
# VITE_APP_BASE_APPURL=https://ycsb-gw-uat.dcin-test.digitalyili.com
# VITE_APP_APPKEY=62f495a0f7854e4e46ebbf40
# VITE_APP_APPID=m5c66hlce3
VITE_APP_AIAGENTURL=/aiagent/assistant/1179e9d7-fb94-4fe4-abbb-55be8f8ca5d6/share

View File

@@ -31,3 +31,4 @@ VUE_APP_JSONPURL = 'https://iam-uat.dctest.digitalyili.com/idp/restful/getIDPTok
VUE_APP_YQRURL = 'https://ocp-uat-ain.digitalyili.com'
VITE_APP_AIAGENTURL=/aiagent/assistant/78907182-cc42-4072-abae-86ef67c1ecd3/share

View File

@@ -1,4 +1,4 @@
FROM yldc-docker.pkg.coding.yili.com/pubrepo/pubdocker/nginx:latest
FROM yldc-docker.pkg.coding.yili.com/pubrepo/pubdocker/nginx:op
# ---------------------只改此处,其它不动--------------------------
# 定义构建物目录 当前目录 ./ 为代码根目录
@@ -14,6 +14,6 @@ COPY ${DIST_DIR} /var/www/html/${URL_DIR}
RUN sed -i "s/\ \/index.html/\ \/${URL_DIR}\/index.html/g" /etc/nginx/conf.d/default.conf
# 默认不建议使用自定义nginx配置如必须使用请提前沟通
# COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY ./docker/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
EXPOSE 80

43
docker/default.conf Normal file
View File

@@ -0,0 +1,43 @@
server {
listen 80 default_server;
server_name _;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
location / {
root /var/www/html;
index index.html index.htm;
try_files $uri $uri/ //index.html;
}
# 启用访问日志,并指定日志文件路径和格式
access_log /var/log/nginx/host.access.log;
# 打印错误日志文件信息
error_log /var/log/nginx/host.error.log;
location /yllog {
root /var/log/nginx;
autoindex on;
}
location /aiagent/ {
proxy_pass https://yiligpt.x.digitalyili.com;
proxy_set_header Host yiligpt.x.digitalyili.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 定义日志格式(通常在 http 块中定义即可)
# log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /var/www/html;
}
}

View File

@@ -6,8 +6,10 @@
"scripts": {
"dev": "vite --mode development",
"uat": "vite --mode uat",
"prod": "vite --mode prod",
"build": "run-p \"build-only {@}\" --",
"build_uat": "vite build --mode uat",
"build_prod": "vite build --mode prod",
"preview": "vite preview",
"build-only": "vite build",
"lint": "npx eslint -c ./node_modules/@yl/yili-fe-lint-config/eslintrc.vue3.js \"src/**/*.{js,ts,tsx,vue,html}\" --fix",

BIN
public/yl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,6 +1,6 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--primary-color: #70B937;
--primary-color: #70b937;
--van-primary-color: #71b73c;
--vt-c-white: #fff;
--vt-c-white-soft: #f8f8f8;
@@ -45,6 +45,7 @@
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
--el-select-input-focus-border-color: var(--primary-color);
--el-color-primary: var(--primary-color);
}
@media (prefers-color-scheme: dark) {

View File

@@ -51,7 +51,7 @@ a,
top: 0;
width: 100%;
// background-color: linear-gradient(to bottom, theme.$theme-color 200px, theme.$nav-color 300px);
background: url("../img/home/nav.png") theme.$nav-color;
background: url('../img/home/nav.png') theme.$nav-color;
color: #000;
& .van-nav-bar__content {

View File

@@ -300,3 +300,12 @@ input {
background: linear-gradient(180deg, #e8fad7 0%, #ffffff 8%);
}
//background: linear-gradient( 180deg, #E8FAD7 0%, #FFFFFF 100%);
.el-input-number__decrease,
.el-input-number__increase {
display: none;
}
.el-input-number .el-input__wrapper,
.el-input-number.is-controls-right .el-input__wrapper {
padding: 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,34 +1,35 @@
<script setup lang="ts">
import { computed, ref, useTemplateRef, watch } from 'vue';
import { computed, nextTick, onErrorCaptured, ref, useTemplateRef, watch } from 'vue';
import { useSetPieChart } from '@/hooks/chart/usePieChart';
import {
formatData,
getTableData,
setDimensionData
} from '@/views/Survey/views/Analysis/components/AnalysisInfo/hooks/pieSeries';
import EmptyContainer from '@/views/Survey/components/EmptyContainer.vue';
// series 信息
const tableData = ref([]);
const analysis = defineModel<any>('analysis');
// console.log('analysis', analysis.value);
const pieChart = useTemplateRef<HTMLSpanElement>('pieChart');
// series 信息
const series = ref([]);
const dimension = defineModel('dimension');
// 图标高度
const chartHeight = computed(() => {
// 需要增加的高度, 由于字多了他就会高度异常,所有要指定一个动态高度
let addHeight = 0;
const optionLength = analysis.value?.option?.length;
// const optionLength = analysis.value?.option?.length;
// 做一些额外的检测, 如果 option 下面的title 字段超过 8 个,就把 addHeight 增加
analysis.value?.option?.forEach((item: any) => {
if (item.title?.length > 8) {
addHeight += 9;
addHeight += 2;
}
});
// 每三个选项高度增加 20px 默认 300px
return dimension.value ? 280 : 280 + (optionLength - 1) * addHeight;
return dimension.value ? 280 : 280 + addHeight;
});
const index = ref(0);
@@ -37,21 +38,22 @@ const index = ref(0);
watch(
() => analysis.value,
async (value) => {
// 排除空数据渲染图标步骤
tableData.value = {
...analysis.value,
option: getTableData(analysis.value)
};
// console.log(`图标的高度是`, chartHeight.value);
// console.log(`tableData.value`, tableData.value);
series.value = formatData(dimension.value ? tableData.value : analysis.value, index.value);
const pieChart = useTemplateRef<HTMLSpanElement>('pieChart');
// console.log(`series value `, series.value);
// console.log(`series value`, series.value);
if (!series.value?.data?.length) return;
if (!series.value.data?.length) return;
// nextTick(() => {
// setTimeout(() => {
useSetPieChart(pieChart, series, { title: false, legend: false });
// }, 1000);
// });
},
{ immediate: true }
);
@@ -60,12 +62,53 @@ const changeChart = (i: number) => {
index.value = i;
series.value = formatData(tableData.value, index.value);
// console.log(`series value. by changeChart`, series.value);
if (series.value.data.length <= 0) {
series.value.data = [{ value: 0, name: 0 }];
}
// const pieChart = useTemplateRef<HTMLSpanElement>('pieChart');
// useSetPieChart(pieChart, series, { title: false, legend: false });
};
const chartVisible = computed(() => {
/**
* 针对特殊题型做处理,
* 首先就是矩阵的题目 question_type 为 8 9 10
*
* */
if (
analysis.value.question_type === 8 ||
analysis.value.question_type === 9 ||
analysis.value.question_type === 10
) {
// console.log(`series.value on matrix`, series.value?.data);
const data = series.value?.data as { name: any; value: any }[];
// 过滤后的 data 数据,
const filterData = data.filter((item) => item.value != 0 && item.name != 0);
return filterData.length;
// series.value?.data.forEach((item: any`1[]) => {
// if (item.value > 0) {
// return true;
// }
// });
} else if (analysis.value.question_type === 106 || analysis.value.question_type === 5) {
const data = series.value?.data as { name: any; value: any }[];
const filterData = data?.filter((item) => item.value && Number(item.value) > 0);
return filterData?.length;
// console.log(`series.value on nps`, series.value?.data);
}
return series.value?.data?.length;
});
onErrorCaptured((err) => {
console.log(`err`, err);
return false;
});
</script>
<template>
@@ -85,7 +128,7 @@ const changeChart = (i: number) => {
<!-- 图表部分 -->
<div
v-if="series?.data?.length"
v-if="chartVisible"
class="charts"
:style="{
display: 'flex',
@@ -98,10 +141,14 @@ const changeChart = (i: number) => {
ref="pieChart"
:style="{
width: '100%',
height: series?.data?.length ? chartHeight + 'px' : ''
height: chartVisible ? chartHeight + 'px' : ''
}"
></span>
</div>
<!-- 图标没有实例就开始展示空图标 -->
<div v-else>
<empty-container :error-msg="'本题暂无有效答题数据'" />
</div>
</section>
</template>

View File

@@ -10,11 +10,6 @@
<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
@@ -54,14 +49,6 @@ import { defineProps, onMounted, ref } from 'vue';
import { deleteTemplate } from '@/api/home/index.js';
import { showDialog, showFailToast, showSuccessToast } from 'vant';
import { useRouter } from 'vue-router';
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 { info, marketItem } = defineProps({
@@ -77,7 +64,8 @@ const { info, marketItem } = defineProps({
});
const userInfo = ref({ userName: '' });
const toDetail = (item) => {
// return false
const marketList = JSON.parse(sessionStorage.getItem('marketList'));
const marketItem = marketList.find((market) => item.scene_code_info === market.code);
router.push({
path: '/templatePreview',
query: {
@@ -111,20 +99,6 @@ const deleteItem = (item) => {
// on cancel
});
};
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(() => {
userInfo.value = JSON.parse(sessionStorage.getItem('userInfo'));

View File

@@ -17,14 +17,14 @@ const navigation = ref([
icon: 'home-o',
inactive: ''
},
{
title: '伊调研',
// link: {
// name: 'home',
// path: '/home'
// },
icon: yl
},
// {
// title: '伊调研',
// // link: {
// // name: 'home',
// // path: '/home'
// // },
// icon: yl
// },
{
title: '我的',
link: {
@@ -65,29 +65,31 @@ watch(activeTab, (value) => {
<template>
<van-tabbar v-model="activeTab" class="navigation">
<template v-for="(item, index) in navigation" :key="item.title">
<span
v-if="index === 1"
style="
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
"
>
<img :src="item.icon" alt="" style="width: 30px; height: 30px" />
{{ item.title }}
</span>
<van-tabbar-item v-else :icon="item.icon" :name="item.title">
{{ item.title }}
</van-tabbar-item>
</template>
<div class="nav-content">
<template v-for="(item, index) in navigation" :key="item.title">
<!-- <span
v-if="index === 1"
style="
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
"
>
<img :src="item.icon" alt="" style="width: 30px; height: 30px" />
{{ item.title }}
</span> -->
<van-tabbar-item :icon="item.icon" :name="item.title">
{{ item.title }}
</van-tabbar-item>
</template>
</div>
</van-tabbar>
</template>
<style scoped lang="scss">
.navigation {
padding: 3px 0;
padding: 10px 0;
// width: 100vw;
// background-color: white;
// display: flex;
@@ -97,6 +99,11 @@ watch(activeTab, (value) => {
// bottom: 0px;
// z-index: 10;
}
.nav-content {
display: flex;
width: 80%;
margin: 0 auto;
}
.navigation-item {
display: flex;

View File

@@ -168,6 +168,7 @@ defineExpose({
<!-- :navigation="navigationConfig" -->
<swiper-container
ref="swiperRef"
:autoHeight="true"
:slides-per-view="slidesPerView"
:space-between="spaceBetween"
:centered-slides="centeredSlides"
@@ -255,7 +256,7 @@ defineExpose({
align-items: center;
justify-content: center;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
color: #409eff;
color: #71b73c;
}
.yl-swiper-pagination {
@@ -278,7 +279,7 @@ defineExpose({
transition: all 0.3s;
&.is-active {
background-color: #409eff;
background-color: #71b73c;
width: 16px;
border-radius: 4px;
}

View File

@@ -106,6 +106,26 @@ function tooptipFormatter(data: { row: any; column: any; cellValue: any }): VNod
content: data
});
}
// render
const RenderSlot = {
functional: true,
props: {
row: Object,
render: Function,
index: Number,
column: {
type: Object,
default: null
}
},
setup(props) {
return () => {
const { row, index, column } = props;
return props.render(h, { row, index, column });
};
}
};
</script>
<template>
@@ -131,15 +151,22 @@ function tooptipFormatter(data: { row: any; column: any; cellValue: any }): VNod
:width="item.width"
:prop="item.prop"
:label="item.label"
show-overflow-tooltip
:show-overflow-tooltip="item.tooltip ? item.tooltip : true"
:sortable="item.sortable"
>
<template #default="scope">
<slot name="column-default" :scope="scope">
<div
class="table-view-html"
@click.stop="handleImageClick"
v-html="scope.row[item.prop]"
></div>
<div v-if="!item.render" @click.stop="handleImageClick">
<span>{{ scope.row[item.prop] }}</span>
</div>
<RenderSlot
v-else
:row="scope.row"
:render="item.render"
:index="scope.$index"
:column="item"
></RenderSlot>
</slot>
</template>
</el-table-column>
@@ -148,9 +175,14 @@ function tooptipFormatter(data: { row: any; column: any; cellValue: any }): VNod
</template>
<style>
.table-view-html {
display: flex;
//align-items: center;
flex-direction: row;
& img {
width: 50px;
height: 100%;
margin-right: 10px;
}
}
</style>
@@ -165,9 +197,16 @@ function tooptipFormatter(data: { row: any; column: any; cellValue: any }): VNod
border-radius: 8px 8px 0 0;
}
:deep(.cell),
:deep(.table-view-html) {
.table-view-html {
display: flex;
justify-content: center;
align-items: center;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 12px;
}
.table-col {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
// font-size: 12px;
}
</style>

View File

@@ -4,13 +4,13 @@ const content = defineModel<{ row: any; column: any; cellValue: any }>('content'
<template>
<div class="tooltip-content">
<span v-html="content?.row?.logic_text"></span>
<span v-html="content?.cellValue"></span>
</div>
</template>
<style lang="scss" scoped>
.tooltip-content {
width: 90vw;
max-width: 80vw;
max-height: 45vh;
overflow: scroll;
}

View File

@@ -0,0 +1,139 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { showImagePreview } from 'vant';
const prop = defineModel<any[]>('prop');
const data = defineModel<any[]>('data');
const tableWidth = ref(0);
const doc_content = ref();
let startX = 0;
const isHorizontal = ref(false);
function handleTouchStart(event: TouchEvent) {
startX = event.touches[0].clientX;
isHorizontal.value = false;
}
function handleImageClick(e: Event) {
if (e.target instanceof HTMLImageElement) {
const imgSrc = e.target.src;
showImagePreview({
images: [imgSrc],
closeable: true
});
}
}
function handleTouchMove(event: TouchEvent) {
const deltaX = event.touches[0].clientX - startX;
// 如果是横向滑动,则阻止冒泡
if (Math.abs(deltaX) > 10) {
isHorizontal.value = true;
event.stopPropagation(); // 阻止事件冒泡到 van-swipe
}
}
watch(
() => data.value,
(newVal) => {
let width = document.getElementsByClassName('doc_content')[0]?.clientWidth
? document.getElementsByClassName('doc_content')[0]?.clientWidth
: document.body.clientWidth;
tableWidth.value = prop.value.length * 100;
if (prop.value.length <= 3) {
// tableWidth.value 判断是否铺满横屏 没有就铺满
tableWidth.value = width;
} else {
tableWidth.value = width - prop.value.length * 100 > 0 ? width : prop.value.length * 100;
}
},
{
deep: true,
immediate: true
}
);
</script>
<template>
<div
style="overflow: auto; width: 100%"
ref="doc_content"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
class="doc_content"
>
<div
:style="{
width: tableWidth + 'px',
overflow: 'auto'
}"
>
<table style="width: 100%; text-align: center; border: none" class="el-table">
<thead class="tableThead el-table__header">
<tr>
<th v-for="item in prop" class="el-table__cell">
<div class="cell" style="font-size: 13px">{{ item.label }}</div>
</th>
</tr>
</thead>
<tr v-for="(item, index) in data" :key="index">
<template v-for="(t, index) in prop">
<td :key="index" v-if="item[index] !== '未知行'" class="el-table__cell">
<div
class="cell table-view-html"
v-html="item[t.prop]"
v-if="item[index] !== '未知行'"
@click="handleImageClick"
></div>
</td>
</template>
</tr>
</table>
</div>
</div>
</template>
<style>
.table-view-html {
img {
width: 50px;
height: 100%;
margin-right: 10px;
}
}
.el-table--fit .el-table__inner-wrapper:before {
width: 0;
}
.el-table__empty-text {
font-size: 13px;
}
</style>
<style scoped lang="scss">
.tableThead {
background: #f2f8ee;
font-size: 12px;
& tr {
background: #f2f8ee;
font-size: 12px;
& th {
background: #f2f8ee;
font-size: 10px;
}
}
}
table {
border-collapse: collapse; // 合并边框
border-spacing: 0; // 清除默认间距
margin: 0;
padding: 0;
border-radius: 8px;
& tr {
padding: 10px;
}
}
//.table-view-html {
// display: flex;
// //align-items: center;
// flex-direction: row;
//
//
//}
</style>

View File

@@ -0,0 +1,22 @@
export const barOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {},
yAxis: {
type: 'category',
data: []
},
xAxis: {
type: 'value'
},
series: [
{
data: [],
type: 'bar'
}
]
};

View File

@@ -17,11 +17,17 @@ function useSetPieChart(
let chartInstance: any;
onMounted(() => {
// console.log(`1233313123`,dom.value, series.value);
// 检测边界范围 dom 和 series 是否存在
if (!dom.value && !series) return;
if (!dom.value || !series.value) return;
// 在 dom 挂载之后,显示饼图
chartInstance = chart.init(dom.value);
// 在 dom 挂载之后,显示饼图
if (!chartInstance) {
chartInstance = chart.init(dom.value);
}
// chartInstance = chartInstance ? chartInstance : chart.init(dom.value);
pieOption.series = JSON.parse(JSON.stringify(series.value));
// 设置图表选项
chartInstance.setOption(pieOption, opts);

View File

@@ -0,0 +1,17 @@
import { getQuestionList } from "@/api/survey";
import { ref } from "vue";
function fetchSingleQuestion(sn: string) {
const list = ref([])
getQuestionList(sn).then(({ data }) => {
list.value = data.data;
});
return {
list,
}
}
export {
fetchSingleQuestion
}

View File

@@ -12,7 +12,8 @@
@click-left="goBack"
>
<template #left>
<van-icon name="left-long" class-prefix="mobilefont" size="18" style="color: #000" />
<img src="@/assets/img/icon_navigate_back.png" class="back-icon" alt="" />
<!-- <van-icon name="left-long" class-prefix="mobilefont" size="18" style="color: #000" />-->
</template>
</van-nav-bar>
</header>
@@ -85,8 +86,8 @@ function goBack() {
}
.back-icon {
width: 18px;
height: 18px;
width: 24px;
height: 24px;
}
}
</style>

View File

@@ -3,7 +3,7 @@
<!-- title 标题和搜索栏 -->
<header class="header">
<van-nav-bar
:class="[$route.meta.pureBGC ? '' : 'navbar-header']"
:class="redirectHeaderStyle"
:title="$route.meta.title"
left-arrow
safe-area-inset-top
@@ -11,31 +11,64 @@
@click-left="goBack"
>
<template #left>
<van-icon name="left-long" class-prefix="mobilefont" size="18" style="color: #000" />
<img src="@/assets/img/icon_navigate_back.png" class="back-icon" alt="" />
<!-- <van-icon name="left-long" class-prefix="mobilefont" size="18" style="color: #000" />-->
</template>
<template #right v-if="$route.meta.shareAction">
<el-icon
class="nav-layout-right"
@click="route.meta.shareAction && route.meta.shareFunc()"
><RiShareForwardBoxFill
/></el-icon>
style="font-size: 30px; width: 30px; height: 30px"
>
<RiShareForwardBoxFill style="width: 30px; height: 30px" />
</el-icon>
</template>
</van-nav-bar>
</header>
<!-- content -->
<div class="redirect-body" :style="{ background: $route.meta.pureBGC ? 'white' : '' }">
<div class="redirect-body" :style="redirectBodyStyle">
<RouterView />
</div>
</div>
</template>
<script setup>
<script setup lang="ts">
import { RiShareForwardBoxFill } from 'vue-icons-plus/ri';
import { RouterView, useRoute } from 'vue-router';
import appBridge from '@/assets/js/appBridge';
import { computed } from 'vue';
import type { CSSProperties } from 'vue';
const route = useRoute();
console.log(route.meta.pureBGC);
// console.log(route.meta.pureBGC);
/**
* 控制 body 的 style
*/
const redirectBodyStyle = computed(() => {
// 背景色控制
const bgc = route.meta.bgc as string;
// 是否纯色背景
const pureBGC = route.meta.pureBGC;
// style 样式
const style = {} as CSSProperties;
if (bgc) style.background = bgc;
else if (pureBGC) style.background = '#fff';
return style;
});
/**
* 控制 header 的 style
*/
const redirectHeaderStyle = computed(() => {
let css = [];
if (route.meta.header?.bgc === 'green') css.push('green-header');
else if (route.meta.header?.pureBGC) {
console.log('pureBGC');
} else css.push('navbar-header');
return css;
});
function goBack() {
if (window.history.length > 1 && route.name !== 'home') {
@@ -72,11 +105,21 @@ const handlePopState = () => {
color: #333;
.nav-layout-right {
:deep(svg) {
width: 20px !important;
height: 20px !important;
stroke-width: 1px !important;
}
}
}
.green-header {
background-color: theme.$theme-color;
}
.redirect-body {
overflow: scroll;
width: 100vw;
height: calc(100vh - var(--sticky-top-height) - 1px);
background: transparent;
}
@@ -94,8 +137,8 @@ const handlePopState = () => {
}
.back-icon {
width: 18px;
height: 18px;
width: 24px;
height: 24px;
}
}
</style>

View File

@@ -1,7 +1,7 @@
import 'amfe-flexible';
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import { createApp } from 'vue';
import { createApp, inject } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
@@ -12,29 +12,49 @@ import appBridge from '@/assets/js/appBridge';
import VConsole from 'vconsole';
import './assets/css/main.scss';
// 引入 swiper 样式
import 'swiper/css';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import { sensorsData } from '@/utils/plugins/sa';
const app = createApp(App);
if (import.meta.env.VITE_APP_ENV !== 'production') {
const vconsole = new VConsole();
/* const vconsole = */ new VConsole();
// app.use(vconsole);
}
declare global {
interface Window {
sa: any;
onAndroidBack: (() => void) | null;
appBridge?: any;
}
}
const sa = {
register: false,
instance: window.sa || null
};
// 定义路由是否可以返回的判断
const routerCanGoBack = () => {
const position = router.options.history.state?.position;
return typeof position === 'number' && position > 0;
};
router.beforeEach((to, from, next) => {
// 神策数据埋点
if (!sa.register && sessionStorage.getItem('userInfo')) {
sa.instance = window.sa;
// 检测是否使用神策的登陆
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '');
sa.instance.setOnceProfile({ loginID: userInfo.userCode });
sa.register = true;
}
if (to.meta?.title) document.title = to.meta.title as string;
if (to.query.digitalYiliToken) {
utils.setSessionStorage('xToken', to.query.digitalYiliToken);
}
@@ -49,6 +69,14 @@ router.beforeEach((to, from, next) => {
};
next();
});
app.use(createPinia());
app.use(router);
// 神策数据插件
app.use(sensorsData(), {
// 测试环境
server_url: 'https://digitaldmo.yili.com/sa?project=sensorstest'
// 正式环境
// server_url: 'https://digitaldmo.yili.com/sa?project=YIP'
});
app.mount('#app');

View File

@@ -2,7 +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';
import type { title } from 'process';
import { getWXShareConfig } from '@/utils/share';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -53,14 +53,26 @@ const router = createRouter({
{
path: '/create',
name: 'create',
meta: { title: '问卷编辑' },
meta: {
title: '问卷编辑',
pureBGC: true,
bgc: '#71b73c',
header: {
bgc: 'green',
pureBGC: false
}
},
component: () => import('../views/Survey/views/Create/Index.vue')
},
{
path: '/templatePreview',
name: 'templatePreview',
meta: {
title: '模板预览'
title: '模板预览',
header: {
bgc: 'green',
pureBGC: false
}
},
component: () => import('@/views/Design/Preview.vue')
},
@@ -68,7 +80,11 @@ const router = createRouter({
path: '/preview',
name: 'preview',
meta: {
title: '预览'
title: '预览',
header: {
bgc: 'green',
pureBGC: false
}
},
component: () => import('@/views/Survey/views/Preview/Index.vue')
},
@@ -92,14 +108,21 @@ const router = createRouter({
{
path: '/search',
name: 'search',
meta: {},
meta: {
},
component: () => import('@/views/HomeSearch/Index.vue')
},
{
path: '/templateMarket',
name: 'templateMarket',
meta: {
pureBGC: true
title: '更多模板',
pureBGC: true,
header: {
pureBGC: true
},
bgc: 'transparent'
},
component: () => import('@/views/Home/components/Market/Index.vue')
},
@@ -121,13 +144,7 @@ const router = createRouter({
shareFunc: () => {
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(
JSON.stringify({
type: 'shareModal',
title: '分享标题',
description: '分享副标题',
thumbImageUrl: 'https://logo.png',
webpageUrl: window.location.href
})
JSON.stringify(getWXShareConfig())
);
}
}
@@ -141,6 +158,12 @@ const router = createRouter({
name: 'design',
meta: {},
component: Design
},
{
path: '/share/:code',
name: 'share',
meta: {},
component: () => import('@/views/Share/Index.vue')
}
]
});

View File

@@ -48,18 +48,18 @@ export const useQuestionStore = defineStore('questionStore', () => {
// 更新数据
async function getQuestions() {
const _url = new URL(location.href).pathname;
const urlParamSearch = new URL(location.href).searchParams;
let { data } = await AnswerApi.getQuetions({
id: urlParamSearch.get('sn'),
data: {
is_preview: 1,
is_template: 0,
is_preview: _url.includes('templatePreview') ? 0 : 1,
is_template: _url.includes('templatePreview') ? 1 : 0,
// is_template: urlParamSearch.get('is_template') || 0,
source: urlParamSearch.get('source') ?? ''
}
});
data = data.data;
console.log(`data:`, data);
// 多语言
data.languageType = [
data?.survey?.style?.is_en_tips ? 'en' : '',

View File

@@ -13,7 +13,7 @@ export default {
select_random: 0,
min_number: 1,
max_number: 1,
min_size: 0,
min_size: 1,
max_size: 1,
is_file: 0,
file_type: '0'

View File

@@ -1,9 +1,9 @@
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import type { ComposeOption } from 'echarts/core';
import * as echarts from 'echarts/core';
import type { PieSeriesOption } from 'echarts/charts';
import type { BarSeriesOption, PieSeriesOption } from 'echarts/charts';
// 引入 饼状图
import { PieChart } from 'echarts/charts';
import { PieChart, BarChart} from 'echarts/charts';
// 组件类型的定义后缀都为 ComponentOption
import type {
DatasetComponentOption,
@@ -30,6 +30,7 @@ import { SVGRenderer } from 'echarts/renderers';
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
type ECOption = ComposeOption<
| PieSeriesOption
| BarSeriesOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
@@ -48,6 +49,7 @@ echarts.use([
UniversalTransition,
SVGRenderer,
PieChart,
BarChart,
LegendComponent
]);

25
src/utils/imgMap.ts Normal file
View File

@@ -0,0 +1,25 @@
import png11 from '@/assets/img/market/11.png';
import png13 from '@/assets/img/market/13.png';
import png14 from '@/assets/img/home/14.png';
import png15 from '@/assets/img/market/15.png';
import png16 from '@/assets/img/market/16.png';
import png17 from '@/assets/img/market/17.png';
import png18 from '@/assets/img/market/18.png';
import png31 from '@/assets/img/home/31.png';
const map = new Map([
[11, png11],
[13, png13],
[14, png14],
[15, png15],
[16, png16],
[17, png17],
[18, png18],
[31, png31]
]);
const imgMap = (code: number) => {
return map.get(code) || png11;
};
export { imgMap };

View File

@@ -0,0 +1,71 @@
import type { App } from 'vue';
import sensors from './dist/v2/sensorsdata.es6';
/**
* 神策数据 SDK
*/
export type Sensors = typeof sensors;
/**
* 创建神策数据插件
*/
export function sensorsData() {
return {
install(app: App, options: any) {
// 初始化神策 SDK
sensors.init({
show_log: true,
is_track_single_page: false,
use_client_time: true,
send_type: 'beacon',
heatmap: {
clickmap: 'not_collect',
scroll_notice_map: 'not_collect'
},
...(options || {})
});
// 注册页面公共属性
sensors.registerPage({
product_name: '伊调研',
platform_type: 'PC'
});
// 提供全局注入的 sensors 实例
app.provide('sensors', sensors);
// 注册到window中
(globalThis as any).sa = sensors;
// 注册 saTrack 自定义指令
registerDirective(app);
}
};
}
/**
* 注册 saTrack 指令,用于点击埋点
* @param {App} app - Vue 应用实例
*/
function registerDirective(app: App) {
function bindTrackListener(binding: any) {
return () => {
const { value: properties } = binding;
sensorsTrack(properties);
};
}
app.directive('sensorsTrack', {
mounted(el, binding) {
el.addEventListener('click', bindTrackListener(binding));
},
unmounted(el, binding) {
// 清除绑定的事件
el.removeEventListener('click', bindTrackListener(binding));
}
});
}
function sensorsTrack(properties: any) {
sensors.track('YiliResearch_PageClick', properties);
}
export { sensors, sensorsTrack };

36
src/utils/share/index.ts Normal file
View File

@@ -0,0 +1,36 @@
type WXShareConfig = {
type: string;
title: string;
description: string;
thumbImageUrl: string;
webpageUrl: string;
};
let config: WXShareConfig = {
type: 'shareModal',
title: '分享标题',
description: '',
thumbImageUrl: 'https://logo.png',
webpageUrl: window.location.href.replace('/ad/', '/share/')
};
// 设置分享配置
export const setWXShareConfig = (cb?: (config: WXShareConfig) => WXShareConfig) => {
// 留作扩展
cb && (config = cb(config));
const url = new URL(window.location.href);
// 获取 url 图片
config.thumbImageUrl = url.origin + '/yl.png';
// 获取 title 内容
config.title = document.title || '伊调研';
// 描述区域待定
config.description = '';
// 网页地址
config.webpageUrl = window.location.href.replace('/ad/', '/share/');
};
// 获取分享配置
export const getWXShareConfig = () => {
setWXShareConfig();
return config;
};

3
src/utils/url/tools.ts Normal file
View File

@@ -0,0 +1,3 @@
export function isCollectUrl(urls: string[]) {
return urls.some((url) => location.href.includes(url));
}

View File

@@ -159,6 +159,27 @@ export function debounce(fn, wait) {
};
}
/**
* 节流函数
* @param fn {Function} 需要节流的函数
* @param wait {number} 节流时间
* @returns {Function} 节流后的函数
*/
export function throttle(fn, wait) {
let lastTime = 0;
return function() {
const context = this;
const args = arguments;
const now = Date.now();
if (now - lastTime >= wait) {
console.log(`throttle`, now - lastTime);
fn.apply(context, args);
lastTime = now;
}
};
}
/**
* 格式化时间
* @param type now当前时间 endOfDay当前时间的 23:59:59

View File

@@ -1,19 +1,44 @@
<script setup lang="ts">
import { fetchBanners } from '@/hooks/request/banner';
import { sensorsTrack } from '@/utils/plugins/sa';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
const router = useRouter();
const { banners } = fetchBanners();
const route = useRoute();
const bannerInfo = computed(() => {
console.log(banners.value);
return banners.value?.find((item: any) => item.code === route.params.code);
});
// 当前是否处于分享页面
const hasShare = defineModel<boolean>('hasShare', { default: false });
function handleButtonClick() {
saTrack(bannerInfo.value);
if (!bannerInfo.value?.url) {
router.push({ name: 'intelligentGeneration' });
return;
}
window.location.href = bannerInfo.value?.url;
}
function saTrack(record: any) {
sensorsTrack({
page_name: 'APP落地页',
model_name: record?.code || '',
button_name: '立即体验'
});
}
/**
* 在挂载之后重新修改 html title 内容
*/
setTimeout(() => {
document.title = bannerInfo.value?.title || '伊调研';
}, 800);
</script>
<template>
<section class="banner-container">
<section class="banner-container" :style="{ background: hasShare ? 'white' : undefined }">
<section class="msg-info">
<h3>{{ bannerInfo?.title }}</h3>
<el-space spacer="|">
@@ -24,13 +49,13 @@ const bannerInfo = computed(() => {
<!-- banner内容 -->
<article>
<!-- 根据banner的类型使用不同的方式渲染 -->
<section class="banner-text" v-if="bannerInfo?.type === 0">
<section v-if="bannerInfo?.type === 0" class="banner-text">
<p>{{ bannerInfo.synopsis }}</p>
</section>
<div class="banner-image" v-else-if="bannerInfo?.type === 1">
<div v-else-if="bannerInfo?.type === 1" class="banner-image">
<el-image fit="cover" loading="lazy" :src="bannerInfo.file_address" />
</div>
<section class="banner-video" v-else-if="bannerInfo?.type === 2">
<section v-else-if="bannerInfo?.type === 2" class="banner-video">
<video width="100%" height="auto" controls>
<source :src="bannerInfo.file_address" type="video/mp4" />
</video>
@@ -38,20 +63,16 @@ const bannerInfo = computed(() => {
</article>
<section
v-if="bannerInfo?.is_display_button"
style="
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
left: 0;
bottom: 10px;
"
v-if="bannerInfo?.is_display_button && !hasShare"
style="margin-top: 20px; width: 100%; position: sticky; bottom: 10px"
>
<!-- 立即进入 -->
<el-button style="width: 95%; height: 50px; border-radius: 15px" color="#70b937">
<el-text style="color: white">{{ bannerInfo.button_name }}</el-text>
<el-button
style="width: 100%; height: 50px; border-radius: 15px"
color="#70b937"
@click="handleButtonClick"
>
<el-text style="color: white">{{ bannerInfo?.button_name }}</el-text>
</el-button>
</section>
</section>
@@ -62,8 +83,6 @@ const bannerInfo = computed(() => {
.banner-container {
padding: $gap * 2;
margin-bottom: $gap * 6;
.msg-info {
font-family: PingFangSC-Medium;
h2 {

View File

@@ -20,7 +20,7 @@
<script setup lang="ts">
import preview from '@/views/Survey/views/Preview/Index.vue';
import { useTemplate } from '@/api/home';
import { saveQuestions, snQuestions } from '@/api/design';
import { getSurveyTemplate, saveQuestions, snQuestions } from '@/api/design';
import { useRoute, useRouter } from 'vue-router';
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';
@@ -29,12 +29,19 @@ const counterStore = useCounterStore();
const store = storeToRefs(counterStore);
const route = useRoute();
const router = useRouter();
const pastTemplate = () => {
const pastTemplate = async () => {
const { data } = await getSurveyTemplate(route.query.sn as string, {
is_preview: 0,
is_template: 1
});
const { title, introduction } = data.data.survey;
const query = {
group_id: 0,
source: 1,
project_name: `${route.query.title}问卷 `,
remarks: '为优化活动服务品质,烦请完成问卷,感谢配合',
project_name: title,
remarks: introduction,
scene_code: route.query.parentCode,
scene_code_info: route.query.code,
// 很迷茫 模板新增 tag 空数组 非模板 就是k
@@ -44,12 +51,12 @@ const pastTemplate = () => {
if (res.data) {
snQuestions({ sn: res.data.data.sn }).then((ques) => {
if (ques.data) {
ques.data.data.survey.introduction = `<p>为优化活动服务品质,烦请完成问卷,感谢配合!您的反馈至关重要!</p>`;
ques.data.data.survey.introduction = introduction;
store.questionsInfo.value = ques.data.data;
saveQuestions({
sn: res.data.data.sn,
introduction: ques.data.data.survey.introduction,
title: ques.data.data.survey.title
introduction: introduction,
title: title
}).then((q) => {
if (q.data) {
router.replace({

View File

@@ -53,12 +53,12 @@
input-align="right"
class="action-field"
placeholder="0"
:min="0"
:min="1"
:max="actionQuestion.config.max_size"
@blur="handleMinSizeBlur"
@update:model-value="
(value) => {
actionQuestion.config.min_size = value === '' ? 0 : value;
actionQuestion.config.min_size = value === '' ? 1 : value;
}
"
>
@@ -78,7 +78,7 @@
@blur="handleMaxSizeBlur"
@update:model-value="
(value) => {
actionQuestion.config.max_size = value === '' ? 0 : value;
actionQuestion.config.max_size = value === '' ? 1 : value;
}
"
>

View File

@@ -18,21 +18,46 @@
></contenteditable>
</template>
<template #input>
<textarea
v-if="!element.config.line_height || element.config.line_height != 0"
<el-input-number
v-model="completionValue"
v-if="element.config.text_type === 1"
type="number"
class="other_input"
:placeholder="element.config.placeholder"
:rows="element.config.line_height"
></textarea>
controls-position="right"
:max="element.config.max"
@blur="changeValue"
style="padding: 0; background: #fff"
></el-input-number>
<el-input-number
v-model="completionValue"
v-else-if="element.config.text_type === 2"
:precision="element.config.decimal_few"
:max="element.config.max"
type="number"
controls-position="right"
class="other_input"
@blur="changeValue"
style="padding: 0; background: #fff"
></el-input-number>
<div v-else style="width: 92%">
<textarea
v-if="!element.config.line_height || element.config.line_height != 0"
v-model="completionValue"
class="other_input"
:placeholder="element.config.placeholder"
:rows="element.config.line_height"
></textarea>
</div>
</template>
</van-field>
</div>
</template>
<script setup>
import { toRefs } from 'vue';
import { toRefs, watch } from 'vue';
import { ElInputNumber } from 'element-plus';
import { showFailToast } from 'vant';
const completionValue = defineModel('completionValue', { default: '', type: String });
const props = defineProps({
isPreview: {
@@ -58,12 +83,33 @@ const errorMessage = defineModel('errorMessage', {
type: String,
default: ''
});
// 创建一个本地副本以保存更改
const emit = defineEmits(['update:element']);
const { element } = toRefs(props);
const emitValue = () => {
emit('update:element', element.value);
};
watch(
() => completionValue.value,
() => {
if (!completionValue.value) {
completionValue.value = null;
}
}
);
const changeValue = (values) => {
console.log(element.value.config);
if (
completionValue.value < element.value.config.min &&
element.value.config.min &&
completionValue.value
) {
completionValue.value = null;
showFailToast('请输入大于' + element.value.config.min + '的数字');
}
};
</script>
<style scoped lang="scss">

View File

@@ -34,7 +34,6 @@ const fileLimit = computed(() => {
};
});
console.log(fileLimit.value);
/**
* 上传文件
* @description 上传文件
@@ -75,7 +74,7 @@ function handleFileUpload() {
// 上传文件
// 生成答案
answer.value = files;
question.value.answer = files;
}
}
}

View File

@@ -1,15 +1,40 @@
<template>
<input
type="text"
class="el-input"
style="width: 100%"
:name="`R${rowIndex + 1}`"
:value="getInputValue(rowIndex, colIndex)"
@change="handleMatrixTextChange(rowIndex, colIndex, $event)"
/>
<div>
<!-- <el-input-number-->
<!-- v-model="inputValueMTX"-->
<!-- style="width: 100%"-->
<!-- v-if="element.config.text_type === 1"-->
<!-- controls-position="right"-->
<!-- @change="(e) => ev"-->
<!-- :max="element.config.max"-->
<!-- @blur="changeValue(rowIndex, colIndex)"-->
<!-- ></el-input-number>-->
<!-- <el-input-number-->
<!-- v-model="inputValueMTX"-->
<!-- style="width: 100%"-->
<!-- v-else-if="element.config.text_type === 2"-->
<!-- controls-position="right"-->
<!-- @change="(e) => handleMatrixTextChange(rowIndex, colIndex, { target: { value: e } })"-->
<!-- :precision="element.config.decimal_few"-->
<!-- :max="element.config.max"-->
<!-- @blur="changeValue(rowIndex, colIndex)"-->
<!-- ></el-input-number>-->
<input
type="text"
class="el-input"
style="width: 100%"
:placeholder="element.config.placeholder"
:name="`R${rowIndex + 1}`"
:value="getInputValue(rowIndex, colIndex)"
@change="handleMatrixTextChange(rowIndex, colIndex, $event)"
/>
</div>
</template>
<script setup lang="ts">
import { ElInputNumber } from 'element-plus';
import { ref } from 'vue';
import { showFailToast } from 'vant';
// 接受获取到的 col row 的索引参数
const rowIndex = defineModel('rowIndex', { required: true, default: 0 });
const colIndex = defineModel('colIndex', { required: true, default: 0 });
@@ -21,8 +46,13 @@ const element = defineModel<question>('element', {
/**/
}
});
const inputValueMTX = ref('');
const emit = defineEmits(['update:matrixAnswer', 'update:rowRecord', 'update:element']);
function ev() {
console.log(1231);
}
function getInputValue(row: number, col: number) {
// console.log(`row: ${row}, col: ${col}`);
// console.log(`rowRecord:`, rowRecord.value);
@@ -35,6 +65,7 @@ function handleMatrixTextChange(row: number, col: number, e: Event) {
const target = e.target as HTMLInputElement;
const inputValue = target.value;
console.log(inputValue, 123);
// 获取 colIndexArray
if (!rowRecord.value[row]) {
// 如果没有对应的row创建一个
@@ -46,6 +77,18 @@ function handleMatrixTextChange(row: number, col: number, e: Event) {
// console.log(`rowRecord:`, rowRecord.value);
}
const changeValue = (rowIndex, colIndex) => {
if (
inputValueMTX.value < element.value.config.min &&
element.value.config.min &&
inputValueMTX.value
) {
inputValueMTX.value = null;
showFailToast('请输入大于' + element.value.config.min + '的数字');
handleMatrixTextChange(rowIndex, colIndex, { target: { value: '' } });
}
};
const emitValue = () => {
emit('update:element', element.value);
};
@@ -53,4 +96,15 @@ const emitValue = () => {
<style scoped lang="scss">
@import '@/assets/css/theme';
input[type='text'] {
width: 100%;
padding: 0 5px;
border: none;
border-radius: 4px;
outline: 1px solid #f4f4f4;
&:focus {
outline: 1px solid $theme-color;
}
}
</style>

View File

@@ -1,7 +1,7 @@
import { ref } from 'vue';
const answer = ref<FileList>();
// const answer = ref<FileList>();
const answer = ref<FileList | []>([]);
/**
* 文件限制
* @property {number} max - 最大文件大小

View File

@@ -1,101 +1,101 @@
<script setup>
import { fetchSurveys } from '@/hooks/request/useSurvey';
import CreateSurvey from './components/CreateSurvey/Index.vue';
import NewSurvey from './components/NewSurvey/index.vue';
import { onMounted, ref } from 'vue';
import utils from '@/assets/js/common';
import { getUserInfo } from '@/api/common/index.js';
import { showFailToast } from 'vant';
import appBridge from '@/assets/js/appBridge';
import ImageSlider from './components/ImageSlider/Index.vue';
import SearchBar from '@/components/Search/Index.vue';
import Navigation from '@/components/Navigation/Index.vue';
import HomeRecommend from './components/HomeRecommend/Index.vue';
import MineTask from '@/views/Home/components/MineTask/Index.vue';
import router from '@/router';
const contentShow = ref(false);
// 获取我的问卷数据
const { surveys } = fetchSurveys();
const keyword = ref('');
onMounted(async () => {
if (appBridge.isInReactNative()) {
const appToken = utils.getSessionStorage('xToken');
getUserInfo(appToken)
.then((res) => {
if (res.data) {
contentShow.value = true;
const token = res.data.data.token;
localStorage.setItem('plantToken', token);
utils.setSessionStorage('userInfo', res.data.data);
} else {
contentShow.value = false;
showFailToast(
error.response.data?.message || error.data?.message || error.message || '服务器错误'
);
}
})
.catch((error) => {
contentShow.value = false;
showFailToast(error?.response?.data?.message || error?.message || '服务器错误');
});
} else {
utils.setSessionStorage('xToken', 'f74ba36d7fc3468480648dedba5672ff');
contentShow.value = true;
}
});
function handleSearchClick() {
router.push({ name: 'search' });
}
</script>
<template>
<div v-if="contentShow" class="container-home">
<div class="container-body">
<!-- 搜索栏 -->
<section class="search">
<search-bar placeholder="请输入关键词" :value="keyword" @click="handleSearchClick" />
</section>
<!-- 首页轮播图 -->
<section class="slider">
<image-slider />
</section>
<create-survey :createdNewPage="false" />
<!-- 最新问卷 -->
<!--<last-survey/>-->
<!-- 模板市场 -->
<!-- <Market/> -->
<!--底部新建问卷-->
<NewSurvey />
<!-- 我的问卷 部分 当问卷不存在时显示推荐内容 -->
<mine-task v-if="surveys?.length > 0" :surveys="surveys" />
<home-recommend v-else class="home_recommend" />
<navigation />
</div>
</div>
</template>
<style scoped lang="scss">
@use '@/assets/css/theme';
.container-body {
padding: 0 10px 80px;
}
.search {
margin: 0 -10px 0 -10px;
padding: 10px;
@extend %search-gradient;
}
.slider {
overflow: hidden;
border-radius: theme.$card-radius;
}
.home_recommend {
margin: theme.$gap 0;
}
</style>
<script setup>
import { fetchSurveys } from '@/hooks/request/useSurvey';
import CreateSurvey from './components/CreateSurvey/Index.vue';
import NewSurvey from './components/NewSurvey/index.vue';
import { onMounted, ref } from 'vue';
import utils from '@/assets/js/common';
import { getUserInfo } from '@/api/common/index.js';
import { showFailToast } from 'vant';
import appBridge from '@/assets/js/appBridge';
import ImageSlider from './components/ImageSlider/Index.vue';
import SearchBar from '@/components/Search/Index.vue';
import Navigation from '@/components/Navigation/Index.vue';
import HomeRecommend from './components/HomeRecommend/Index.vue';
import MineTask from '@/views/Home/components/MineTask/Index.vue';
import router from '@/router';
const contentShow = ref(false);
// 获取我的问卷数据
const { surveys } = fetchSurveys();
const keyword = ref('');
onMounted(async () => {
if (appBridge.isInReactNative()) {
const appToken = utils.getSessionStorage('xToken');
getUserInfo(appToken)
.then((res) => {
if (res.data) {
contentShow.value = true;
const token = res.data.data.token;
localStorage.setItem('plantToken', token);
utils.setSessionStorage('userInfo', res.data.data);
} else {
contentShow.value = false;
showFailToast(
error.response.data?.message || error.data?.message || error.message || '服务器错误'
);
}
})
.catch((error) => {
contentShow.value = false;
showFailToast(error?.response?.data?.message || error?.message || '服务器错误');
});
} else {
utils.setSessionStorage('xToken', 'f74ba36d7fc3468480648dedba5672ff');
contentShow.value = true;
}
});
function handleSearchClick() {
router.push({ name: 'search' });
}
</script>
<template>
<div v-if="contentShow" class="container-home">
<div class="container-body">
<!-- 搜索栏 -->
<section class="search">
<search-bar placeholder="请输入关键词" :value="keyword" @click="handleSearchClick" />
</section>
<!-- 首页轮播图 -->
<section class="slider">
<image-slider />
</section>
<create-survey :createdNewPage="false" />
<!-- 最新问卷 -->
<!--<last-survey/>-->
<!-- 模板市场 -->
<!-- <Market/> -->
<!--底部新建问卷-->
<NewSurvey />
<!-- 我的问卷 部分 当问卷不存在时显示推荐内容 -->
<mine-task v-if="surveys?.length > 0" :surveys="surveys" />
<home-recommend v-else class="home_recommend" />
<navigation />
</div>
</div>
</template>
<style scoped lang="scss">
@use '@/assets/css/theme';
.container-body {
padding: 0 10px 80px;
}
.search {
margin: 0 -10px 0 -10px;
padding: 10px;
@extend %search-gradient;
}
.slider {
overflow: hidden;
border-radius: theme.$card-radius;
}
.home_recommend {
margin: theme.$gap 0;
}
</style>

View File

@@ -30,8 +30,10 @@ const createdQuestion = (item) => {
const query = {
group_id: 0,
source: 1,
project_name: `${item.title ? item.title : 'z'} `,
remarks: item.title ? '为优化活动服务品质,烦请完成问卷,感谢配合!您的反馈至关重要!' : '请输入问卷描述',
project_name: `${item.title ? item.title : '请输入问卷标题'} `,
remarks: item.title
? '为优化活动服务品质,烦请完成问卷,感谢配合!您的反馈至关重要!'
: '请输入问卷描述',
scene_code: item.parentCode,
scene_code_info: item.code,
tags: ''
@@ -74,7 +76,7 @@ const createdApx = (res) => {
snQuestions({ sn: res.data.data.sn }).then((ques) => {
console.log(`res`, res);
if (ques.data) {
const {data} = res.data
const { data } = res.data;
ques.data.data.survey.introduction = `<p>${data.remarks}</p>`;
store.questionsInfo.value = ques.data.data;
saveQuestions({
@@ -169,7 +171,7 @@ onMounted(() => {
color: #000;
.create_survey_title {
margin: 16px;
margin: 16px 10px;
font-size: 15px;
font-family: PingFangSC-Heavy;
font-weight: 900;

View File

@@ -1,16 +1,64 @@
<script setup lang="ts">
import { escapeHTML } from '@/utils/stringTranslate';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
const host = `https://yiligpt.x.digitalyili.com`;
const path = '/aiagent/assistant/78907182-cc42-4072-abae-86ef67c1ecd3/share';
const param = `?token=${localStorage.getItem('plantToken')}&source=app`;
const host = window.location.origin;
const path = import.meta.env.VITE_APP_AIAGENTURL + '?';
const param = `token=${encodeURIComponent(localStorage.getItem('plantToken') as string)}&source=app`;
const url = host + path + param;
const iframe = ref<HTMLIFrameElement | null>(null);
const router = useRouter();
onMounted(() => {
// 保存原始的window.open方法
const originalOpen = window.open;
// 字符串转义
// 监听iframe的load事件确保iframe已完全加载
iframe.value?.addEventListener('load', () => {
try {
const iframeWindow = iframe.value?.contentWindow;
// 尝试覆盖iframe的open方法
if (iframeWindow) {
// 方法一:直接覆盖
try {
iframeWindow.open = function (...args: any[]) {
const url = new URL(args[0]); // 使用 URL API 解析
const path = url.pathname; // 获取路径部分
const query = Object.fromEntries(url.searchParams); // 将 search 转换为对象
// 如果需要去除特定 host 前缀(如本地调试)
if (url.host === window.location.host) {
args[0] = args[0].replace(`https://${url.host}`, '');
}
router.push({
path,
query
});
};
} catch (e) {
// 方法二如果直接覆盖失败尝试使用defineProperty
try {
Object.defineProperty(iframeWindow, 'open', {
value: function (...args: any[]) {
// console.log('iframe中的open方法被调用参数', args);
return originalOpen.apply(window, args as any);
},
writable: true,
configurable: true
});
} catch (e2) {
console.error('无法覆盖iframe的open方法(defineProperty)', e2);
}
}
}
} catch (error) {
console.error('无法覆盖iframe的open方法', error);
}
});
});
</script>
<template>
<iframe style="height: 100%; width: 100%" :src="escapeHTML(url)" frameborder="0" />
<iframe ref="iframe" style="height: 100%; width: 100%" :src="url" frameborder="0"></iframe>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,93 +1,151 @@
<script setup lang="ts">
import { recommend } from '@/hooks/request/recommend';
import { ref } from 'vue';
import YlTable from '@/components/YlTable/Index.vue';
import CommonLayout from '@/components/Layout/CommonLayout.vue';
// 外部获取的数据
const { data } = recommend({});
const props = ref<TablePropsType[]>([
{
prop: 'rank',
label: '排名',
width: 58
},
{
prop: 'trend_name',
label: '趋势名称',
width: 100
},
{
prop: 'growth_ring_ratio',
label: '声量增长环比',
width: 120
},
{
prop: 'sales_growth_ring_ratio',
label: '销量增长环比',
width: 120
}
]);
</script>
<template>
<van-cell class="home_recommend">
<template #extra>
<div style="width: 90vw">
<common-layout title="123">
<template #title>
<h3 class="recommend-layout-title">内容推荐</h3>
</template>
<span class="recommend-title">{{ data?.title || 'TOP5现制饮品风味' }}</span>
<yl-table
:header-style="{ background: '#E8F9F4' }"
:data="data?.surveyTrendDataVOS"
:props="props"
/>
<!-- 剧中展示提示语 -->
<div style="width: 100%; margin-top: 10px; text-align: center">
<span style="">- 最新数据及更多创新趋势请到YIP探索 - </span>
</div>
</common-layout>
</div>
</template>
</van-cell>
</template>
<style lang="scss" scoped>
@use '@/assets/css/theme' as *;
.home_recommend {
border-radius: $card-radius;
justify-content: center;
align-items: center;
.recommend-layout-title {
font-family:
PingFangSC,
PingFang SC;
font-weight: 800;
font-size: 15px;
color: #000000;
line-height: 20px;
text-align: left;
font-style: normal;
}
.recommend-title {
color: #000000;
font-family:
PingFangSC,
PingFang SC;
font-size: 14px;
font-weight: 800;
line-height: 15px;
text-align: left;
font-style: normal;
margin-bottom: 10px;
}
}
</style>
<script setup lang="ts">
import { recommend } from '@/hooks/request/recommend';
import { ref } from 'vue';
import YlTable from '@/components/YlTable/Index.vue';
import CommonLayout from '@/components/Layout/CommonLayout.vue';
// 外部获取的数据
const { data } = recommend({});
const props = ref<TablePropsType[]>([
{
prop: 'rank',
label: '排名',
width: 58,
render: (h, p) => {
return h(
'div',
{
style: {
color: '#81B64C'
}
},
p.row.rank
);
}
},
{
prop: 'trend_name',
label: '趋势名称',
width: 100
},
{
prop: 'growth_ring_ratio',
label: '声量增长环比',
width: 150,
sortable: true,
render: (h, p) => {
return h(
'div',
{
style: {
color: p.row.growth_ring_ratio > 0 ? '#DD6E4E' : '#81B64C'
}
},
[
h('span', p.row.growth_ring_ratio + '%'),
h('i', {
class: 'van-icon van-icon-play ml5',
style: {
transform: p.row.growth_ring_ratio > 0 ? 'rotate(-90deg)' : 'rotate(90deg)'
}
})
]
);
}
},
{
prop: 'sales_growth_ring_ratio',
label: '销量增长环比',
width: 150,
sortable: true,
render: (h, p) => {
return h(
'div',
{
style: {
color: p.row.sales_growth_ring_ratio > 0 ? '#DD6E4E' : '#81B64C'
}
},
[
h('span', p.row.sales_growth_ring_ratio + '%'),
h('i', {
class: 'van-icon van-icon-play ml5',
style: {
transform: p.row.sales_growth_ring_ratio > 0 ? 'rotate(-90deg)' : 'rotate(90deg)'
}
})
]
);
}
}
]);
</script>
<template>
<van-cell class="home_recommend">
<template #extra>
<div style="width: 90vw">
<common-layout title="123">
<template #title>
<h3 class="recommend-layout-title">创新速递</h3>
</template>
<span class="recommend-title">{{ data?.title }}</span>
<yl-table
style="margin-top: 10px"
:header-style="{ background: '#E8F9F4' }"
:data="data?.surveyTrendDataVOS"
:props="props"
/>
<!-- 剧中展示提示语 -->
<div class="more">
<span>- 最新数据及更多创新趋势请到YIP探索 - </span>
</div>
</common-layout>
</div>
</template>
</van-cell>
</template>
<style lang="scss" scoped>
@use '@/assets/css/theme' as *;
.home_recommend {
border-radius: $card-radius;
justify-content: center;
align-items: center;
.recommend-layout-title {
font-family:
PingFangSC,
PingFang SC;
font-weight: 800;
font-size: 15px;
color: #000000;
line-height: 20px;
text-align: left;
font-style: normal;
}
.recommend-title {
color: #000000;
font-family:
PingFangSC,
PingFang SC;
font-size: 14px;
font-weight: 800;
line-height: 15px;
text-align: left;
font-style: normal;
}
.more {
margin: 15px 5px 5px 5px;
color: #919191;
font-weight: 400;
font-size: 13px;
text-align: center;
}
}
</style>

View File

@@ -3,6 +3,7 @@ import { useRouter } from 'vue-router';
import { bannerInfo } from '@/views/AD/hooks/useAD';
import { fetchBanners } from '@/hooks/request/banner';
import { computed } from 'vue';
import { sensorsTrack } from '@/utils/plugins/sa';
const { banners } = fetchBanners();
@@ -10,7 +11,7 @@ const router = useRouter();
// const defineBanners = defineModel('banners');
// 如果定义了 banner 那么 banners 就不再初始化
// defineBanners.value && updateBanners(defineBanners.value);
const borderRadius = defineModel('borderRadius');
const borderRadius = defineModel<number>('borderRadius');
// 是否启用平铺展示
const stack = defineModel<boolean>('stack', { default: true });
// 上级传递的 banners
@@ -21,9 +22,19 @@ const limit = defineModel<number>('limit');
function handleBannerClick(banner: any) {
// 把对应的信息给 AD 的 hooks
bannerInfo.value = banner;
saTrack(banner);
router.push({ name: 'ad', params: { code: banner.code } });
}
function saTrack(record: any) {
sensorsTrack({
page_name: 'APP首页',
model_name: record?.code || '',
button_name: '轮播图'
});
}
const currentBanners = computed(() => {
if (bannersList.value) {
return stack.value ? bannersList.value : bannersList.value?.slice(0, limit.value);
@@ -33,27 +44,29 @@ const currentBanners = computed(() => {
</script>
<template>
<van-swipe :autoplay="5000" indicator-color="white" v-if="stack">
<van-swipe-item v-for="banner in currentBanners" :key="banner.code">
<div class="slider-container">
<van-swipe v-if="stack" :autoplay="5000" indicator-color="white">
<van-swipe-item v-for="banner in currentBanners" :key="banner.code">
<el-image
class="img"
:style="{ borderRadius: borderRadius + 'px' }"
:src="banner.banner_address"
fit="contain"
@click="handleBannerClick(banner)"
/>
</van-swipe-item>
</van-swipe>
<div v-else>
<el-image
v-for="banner in currentBanners"
:key="banner.code"
class="img"
:style="{ borderRadius: borderRadius + 'px' }"
:src="banner.banner_address"
fit="contain"
@click="handleBannerClick(banner)"
/>
</van-swipe-item>
</van-swipe>
<div v-else>
<el-image
v-for="banner in currentBanners"
:key="banner.code"
class="img"
:style="{ borderRadius: borderRadius + 'px' }"
:src="banner.banner_address"
fit="contain"
@click="handleBannerClick(banner)"
/>
</div>
</div>
</template>

View File

@@ -2,62 +2,110 @@
<section class="market-container">
<!-- 模板 -->
<div class="market">
<search v-model:value="searchValue" @change="fetchTemplate"></search>
<search v-model:value="searchValue" @search="fetchTemplate"></search>
<van-tabs
v-model:active="active"
style="margin-top: 15px"
class="px-1"
@change="getMarketInfo"
shrink
duration="0"
color="#6fb937"
@change="handleChangeTab"
>
<section>
<van-tab v-for="item in marketList" :key="item.title" :title="item.h5Title">
<van-tab v-for="(item, index) in marketList" :key="item.title" :title="item.h5Title">
<template #title>
<section>{{ item.h5Title }}</section>
<!-- <el-icon><el-img :src="item.h5Image" /></el-icon> -->
<div
style="
display: flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
border-radius: 10px;
"
:style="{ background: active === index ? '#6fb937' : '' }"
>
<!-- 当标签不是处于激活状态的时候取消图片展示 -->
<img v-if="active === index" width="15px" height="15px" :src="imgMap(item.code)" />
<section :style="{ color: active === index ? '#fff' : '#000000' }">
{{ item.h5Title }}
</section>
</div>
</template>
</van-tab>
</section>
</van-tabs>
</div>
<market-item :info="marketInfo" :marketItem="marketItem" />
<section v-infinite-scroll="loadData">
<market-item :info="marketInfo" :marketItem="marketItem" />
</section>
<div class="more">
<p>-更多模板期待您的探索-</p>
<p v-if="marketInfo.length === 0 && searchValue">无符合要求结果</p>
<p v-else>-更多模板期待您的探索-</p>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, watch } from 'vue';
import MarketItem from '@/components/MarketItem/MarketItem.vue';
import { getListScene, getSurveyTemplates } from '@/api/home';
import Search from '@/components/Search/Index.vue';
import { fetchSearchResult } from '@/hooks/request/useSearch';
const searchResult = ref([]);
import { imgMap } from '@/utils/imgMap';
import { throttle } from '@/utils/utils';
import { functionsIn } from 'lodash';
const searchParm = {
index: 1
};
const marketList = ref([]);
const active = ref(null);
const marketIndex = ref(0);
const marketInfo = ref([]);
const marketInfo = ref<any[]>([]);
// 当前激活的 item 信息
const marketItem = ref();
// 当前的搜索指
const searchValue = ref('');
let keyword = '';
let loadDataSingle = false;
/**
* 如果这个搜索框没有值,默认清空 keyword, 然后重新加载列表
*/
watch(searchValue, (value) => {
if (!value) {
keyword = '';
fetchTemplate();
}
});
const getTableList = async () => {
const res = await getListScene();
// 将 tabs 数据存放到 sessionStorage 中
sessionStorage.setItem('marketList', JSON.stringify(res.data.data));
if (res.data.code === 0) {
res.data.data.forEach((item) => {
// if (item.parentCode && item.parentCode === 1) {
marketList.value.push(item);
// }
});
getMarketInfo(marketList.value[0]);
marketList.value = res.data.data;
}
};
watch(active, () => {
console.log(`active change`, active.value);
});
// 开始请求所有 table 的数据
getTableList();
const getMarketInfo = async (item: string | number, title?: string) => {
marketIndex.value = item as number;
const data = marketList.value.filter((market, index) => item === index)[0];
const data = marketList.value[active.value];
if (data) {
const params = {
page: 1,
page: searchParm.index,
// 此字段无法脱离组件使用
keyword,
per_page: 10,
group_id: 0,
is_public: 1,
@@ -67,31 +115,60 @@ const getMarketInfo = async (item: string | number, title?: string) => {
// 获取相应的 Info 信息
const res = await getSurveyTemplates(params);
console.log(`template market res`, res);
const meta = res.data.meta;
const { current_page, last_page } = meta;
if (res.data.code === 0) {
marketInfo.value = res.data.data;
// marketInfo.value = res.data.data;
marketInfo.value = [...marketInfo.value, ...res.data.data];
}
// 获取对应的 item 信息
marketItem.value = marketList.value.find((item) => item.h5Title === title);
// 后置处理. 解构出当前和最后一页内容,然后判断是否相等,如果相等,则取消数据加载
if (current_page === last_page) {
loadDataSingle = false;
} else {
searchParm.index++;
loadDataSingle = true;
}
// 获取对应的 itme 信息
marketItem.value = marketList.value.find((item) => item.title === title);
}
};
/**
* 当 table 切换的时候, 重置当前的参数数据
*/
function handleChangeTab() {
searchParm.index = 1;
marketInfo.value = [];
searchData();
}
/**
* 搜索模板
* @param keyword 搜索关键词
*/
function fetchTemplate() {
// const { templates } = fetchSearchResult(searchValue.value);
// searchResult.value = templates;
// 当点击搜索的时候, 重置当前的参数数据
keyword = searchValue.value;
handleChangeTab();
}
/**
* 这个函数负责调用搜索
*/
function searchData() {
getMarketInfo(marketIndex.value);
}
const debounceFn = throttle(searchData, 500);
function loadData() {
if (!loadDataSingle) return;
debounceFn();
}
onMounted(() => {
getTableList();
});
</script>
<style scoped>
:deep(.van-tabs__line) {
background-color: #70b937;
display: none !important;
}
:deep(.van-tab--active) {
@@ -107,7 +184,7 @@ onMounted(() => {
.market {
padding: 20px;
border-radius: 0 0 16px 16px;
background-color: red;
background-color: #fff;
.market_title {
margin-bottom: 5px;

View File

@@ -1,19 +1,66 @@
<script setup lang="ts">
import QuestionList from './components/QuestionList.vue';
import { ref } from 'vue';
import { ref, watch, nextTick } from 'vue';
import { isDrag } from './hooks/useDragEvent';
const active = ref(0);
const total = ref(0);
const surveys = defineModel('surveys', { required: true });
function setActive(act, tol) {
function setActive(act: number, tol: number) {
active.value = act;
total.value = tol;
}
const swiper = ref();
const questionComat = ref([]);
function handleDragStart() {
isDrag.value = true;
}
watch(
() => active.value,
(value) => {
updateSwiperHeight();
}
);
nextTick(() => {
if (questionComat.value && questionComat.value.length > 0) {
console.log('questionComat 已经加载完成');
updateSwiperHeight();
}
});
function updateSwiperHeight() {
setTimeout(() => {
if (swiper.value && questionComat.value[active.value]) {
console.log(questionComat.value[active.value]?.$el.scrollHeight, '123');
if (questionComat.value[active.value]?.$el.scrollHeight === 0) {
updateSwiperHeight();
return false;
}
swiper.value.$el.style.height =
questionComat.value[active.value]?.$el.scrollHeight + 30 + 'px';
swiper.value.resize();
}
}, 500);
}
function slideChange() {
updateSwiperHeight();
}
function handleDragEnd() {
isDrag.value = false;
// setTimeout(() => {
// // 获取高度
// swiper.value.$el.style.height = questionComat.value[active.value].$el.scrollHeight + 30 + 'px';
// swiper.value.resize();
// }, 500);
// swiper.value.height = questionList.value.;
}
</script>
<template>
<div class="carousel-container">
<div class="title">
<span>我的任务</span>
<span style="margin-top: 6px">我的任务</span>
<div class="carousel-indicators">
<i
v-for="(item, index) in total"
@@ -24,9 +71,15 @@ function setActive(act, tol) {
<!--分页器如果放置在swiper外面需要自定义样式-->
</div>
<div>
<van-swipe :loop="false">
<van-swipe :loop="false" @drag-start="handleDragStart" @drag-end="handleDragEnd" ref="swiper">
<van-swipe-item v-for="question in surveys" :key="question?.sn">
<question-list :survey="question" style="max-width: 100vw; overflow: hidden" />
<question-list
@slideChange="slideChange"
:parentRef="swiper"
:survey="question"
style="max-width: 100vw; overflow: hidden"
ref="questionComat"
/>
</van-swipe-item>
<template #indicator="{ active, total }">
{{ setActive(active, total) }}

View File

@@ -7,13 +7,23 @@ import { fetchSingleSurvey } from '@/hooks/request/useSurvey';
import YlSwiper from '@/components/YlSwiper/Index.vue';
import EmptyContainer from '@/views/Survey/components/EmptyContainer.vue';
import emptyImg from '@/assets/img/emptyImg.png';
import { isDrag } from '../hooks/useDragEvent';
import { defineEmits } from 'vue';
const survey = defineModel<SurveyItem>('survey');
const parentRef = defineModel<any>('parentRef');
// 获取问卷分析数据
const { questionAnalysis } = useFetchAnalysis(survey.value?.sn as string);
const { currentSurvey } = fetchSingleSurvey(survey.value?.sn as string);
const disableInsight = ref(true);
// 定义事件
const emit = defineEmits(['slideChange']);
const slideChange = function (swiper: any) {
parentRef.value.resize();
emit('slideChange', { swiper, activeIndex: swiper.activeIndex });
};
</script>
<template>
@@ -32,16 +42,18 @@ const disableInsight = ref(true);
<section class="analysis-info">
<!-- 方式一使用默认插槽手动添加 swiper-slide 元素 -->
<yl-swiper
@slideChange="slideChange"
:pagination="!isDrag"
:slides-per-view="1"
:centered-slides="true"
:pagination="true"
:navigation="true"
:navigation="!isDrag"
:loop="false"
:autoHeight="true"
:space-between="0"
:allow-touch-move="false"
>
<swiper-slide v-for="analysis in questionAnalysis" :key="analysis.stem">
<analysis-info :sn="survey?.sn" :questionAnalysis="[analysis]" />
<swiper-slide v-for="(analysis, index) in questionAnalysis" :key="analysis.stem">
<analysis-info :sn="survey?.sn" :questionAnalysis="[analysis]" :parentIndex="index + 1" />
</swiper-slide>
<div class="empty-container" v-if="questionAnalysis?.length === 0">
<empty-container :error-msg="'本问卷暂无有效答题数据'" :img-src="emptyImg" />

View File

@@ -0,0 +1,11 @@
import { watch,ref } from "vue";
/**
* 这个是用于判断是否正在拖动,来决定是否显示左右轮播图的按钮
*/
const isDrag = ref(false);
watch(isDrag, (val) => {
// console.log('isDrag', val);
})
export {isDrag}

View File

@@ -4,12 +4,13 @@ import MarketItem from '@/components/TemplateMarketItem/Index.vue';
import { templates } from '../../Hooks/useSurveySearch';
import { consoleSurveys, useTemplate } from '@/api/home';
import { saveQuestions, snQuestions } from '@/api/design';
import { useRouter } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
const router = useRouter();
const route = useRoute();
// 获取 Store 实例
const counterStore = useCounterStore();
@@ -18,6 +19,27 @@ const store = storeToRefs(counterStore as any);
const limit = defineModel<number>('limit');
const currentTemplate = computed(() => templates.value.slice(0, limit.value));
function handleTemplateClick(template: any) {
// ?sn=4O5xanLV&is_template=1&source=4O5xanLV&title=报名签到&parentCode=1&scene_code_info=11&user=苗闻博&quote_nums=3
const isSearch = route.path.includes('/search');
router.push({
path: '/templatePreview',
query: {
sn: template.sn,
user: template.creater_user,
is_template: 1,
source: template.sn,
// H5TITLE 是为搜索页面传入的参数做兼容
title: isSearch ? template.h5_title : template.title,
parentCode: template.parentCode,
scene_code_info: template.scene_code_info,
quote_nums: template.quote_nums
}
});
return;
const query = {
group_id: 0,
source: 1,

11
src/views/Share/Index.vue Normal file
View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
const shareComponent = defineAsyncComponent(() => import('@/views/AD/Index.vue'));
</script>
<template>
<share-component :has-share="true" />
</template>
<style lang="scss" scoped></style>

View File

@@ -1,42 +1,40 @@
<template>
<div class="survey-search">
<nav-search
v-model:value="searchValue"
@search="handleSearchClick"
@cancel="handleCancelClick"
/>
<!-- <nav-search
placeholder="请输入关键词"
v-model:value="searchValue"
@click="() => $router.push({ name: 'search' })"
/> -->
</div>
<div v-loading="requestLoading" class="new-survey-container">
<div style="margin-bottom: 80px">
<van-list v-model:loading="loading" :finished="finished" @load="handleLoadSurveys">
<template #finished>
<!-- 如果存在搜索文字的话显示没有更多了 -->
<span v-if="searchValue">
<el-text>无符合要求的结果</el-text>
</span>
</template>
<template v-if="survey.length > 0">
<div v-for="item in survey" :key="item" class="new-survey_item">
<survey-item :survey="item" :is-analysis="true" :disable-action-button="false" />
</div>
</template>
<!-- 如果问卷等于0的话显示空容器 -->
<empty-container
v-if="survey.length === 0 && !searchValue"
:img-src="emptyImg"
:show-button="true"
@handle-click="handleEmptyClick"
/>
<NewSurvey v-model:show="showModel" />
</van-list>
<section>
<!-- survey container -->
<div class="survey-search">
<nav-search
v-model:value="searchValue"
@search="handleSearchClick"
@cancel="handleCancelClick"
/>
</div>
</div>
<div v-loading="requestLoading" class="new-survey-container">
<div style="margin-bottom: 80px">
<van-list v-model:loading="loading" :finished="finished" @load="handleLoadSurveys">
<template #finished>
<!-- 如果存在搜索文字的话显示没有更多了 -->
<span v-if="searchValue">
<el-text>无符合要求的结果</el-text>
</span>
</template>
<template v-if="survey.length > 0">
<div v-for="item in survey" :key="item" class="new-survey_item">
<survey-item :survey="item" :is-analysis="true" :disable-action-button="false" />
</div>
</template>
<!-- 如果问卷等于0的话显示空容器 -->
<empty-container
v-if="survey.length === 0 && !searchValue"
:img-src="emptyImg"
:show-button="true"
@handle-click="handleEmptyClick"
/>
<NewSurvey v-model:show="showModel" />
</van-list>
</div>
</div>
</section>
</template>
<script setup>
@@ -137,8 +135,8 @@ function handleEmptyClick() {
}
.new-survey-container {
margin: 0px 3px;
// margin: 0px 3px;
overflow: hidden;
.new-survey_item {
margin: 0 10px 10px;
padding: 10px;

View File

@@ -1,40 +1,43 @@
<script setup lang="ts">
import { useCssModule } from 'vue';
const errorMsg = defineModel<string>('errorMsg', { default: ' - 更多任务期待您的创建 - ' });
const showButton = defineModel<boolean>('showButton', { default: false });
const imgSrc = defineModel<string>('imgSrc');
const emit = defineEmits(['handle-click']);
const style = useCssModule();
</script>
<template>
<el-empty>
<template #image v-if="imgSrc">
<slot>
<!-- 如果放了图片默认展示图片位置 -->
<img :src="imgSrc" alt="" :class="style.img" />
</slot>
</template>
<template #description>
<el-text>{{ errorMsg }}</el-text>
</template>
<el-button color="#71b73c" v-if="showButton" @click="emit('handle-click')" class="btn">
<div style="color: #fff">+ 新建问卷</div>
</el-button>
</el-empty>
</template>
<style scoped lang="scss" module>
.img {
width: 30vw;
height: auto;
}
.btn {
color: #fff;
font-weight: 400;
border-radius: 8px;
}
</style>
<script setup lang="ts">
import { useCssModule } from 'vue';
import emptyImg from '@/assets/img/emptyImg.png';
const errorMsg = defineModel<string>('errorMsg', { default: ' - 更多任务期待您的创建 - ' });
const showButton = defineModel<boolean>('showButton', { default: false });
const imgSrc = defineModel<string>('imgSrc', { default: emptyImg });
const emit = defineEmits(['handle-click']);
const style = useCssModule();
</script>
<template>
<el-empty>
<template #image v-if="imgSrc">
<slot>
<!-- 如果放了图片默认展示图片位置 -->
<img :src="imgSrc" alt="" :class="style.img" />
</slot>
</template>
<template #description>
<slot name="description">
<el-text>{{ errorMsg }}</el-text>
</slot>
</template>
<el-button color="#71b73c" v-if="showButton" @click="emit('handle-click')" class="btn">
<div style="color: #fff">+ 新建问卷</div>
</el-button>
</el-empty>
</template>
<style scoped lang="scss" module>
.img {
width: 30vw;
height: auto;
}
.btn {
color: #fff;
font-weight: 400;
border-radius: 8px;
}
</style>

View File

@@ -19,8 +19,9 @@ import {
clearSurveys
} from '@/views/Survey/hooks/useSurveyData';
import ai from '@/assets/img/analysis/ai.svg';
import { ref } from 'vue';
import { computed, onMounted, ref, useTemplateRef, type CSSProperties } from 'vue';
import { formatTime } from '@/utils/date';
import { windowWidth } from 'vant/lib/utils';
const form = ref({
page: 0,
@@ -28,6 +29,8 @@ const form = ref({
project_name: ''
});
const titleRef = useTemplateRef('titleRef');
// router
const router = useRouter();
const route = useRoute();
@@ -54,6 +57,34 @@ function setImg(code: number) {
return imageMap[code] || png11;
}
// 是否显示 title 的 popover
const showTitlePop = ref(false);
// 是否是短标题 title; 在组件挂载之后会根据 title 的宽度来判断
const isShortTitle = ref(false);
onMounted(() => {
// console.log(titleRef.value);
if (titleRef.value) {
const offsetWidth = titleRef.value.$el.offsetWidth;
isShortTitle.value = 120 <= offsetWidth;
}
});
const surveyTitleStyle = computed<CSSProperties>(() => {
const isPublishNumber = survey.value.is_publish_number;
const isSurveyTime = survey.value.is_time;
let width = '';
if (isSurveyTime && isPublishNumber) {
if (isShortTitle.value) {
width = '25vw';
}
} else if (isSurveyTime) {
width = '170px';
}
return {
maxWidth: width
};
});
function editItem(item: SurveyItem) {
router.push({
path: '/create',
@@ -69,8 +100,8 @@ function toPreview(item: SurveyItem) {
query: {
sn: item.sn,
name: item.project_name,
source: 0,
is_template: 1
source: 0
// is_template: 1
}
});
}
@@ -149,7 +180,7 @@ function copyItem(item: SurveyItem) {
<div style="position: relative">
<div style="display: flex; justify-content: space-between; margin: 10px 0">
<div class="survey_item_info_title">
<el-text style="max-width: 100px">
<el-text ref="titleRef" :style="surveyTitleStyle">
<b v-html="survey.project_name"></b>
</el-text>
<el-text v-if="survey.is_publish_number" class="wrap">
@@ -281,6 +312,11 @@ function copyItem(item: SurveyItem) {
position: absolute;
top: -25px;
right: -25px;
img {
transform: translate(0%, -7%);
width: 98px;
}
}
.survey_item_info_title {

View File

@@ -1,105 +1,140 @@
import { getSurveysPage, deleteSurveys, saveTemplates } from '@/api/home';
import { ref } from 'vue';
import { showDialog, showConfirmDialog, showFailToast, showToast } from 'vant';
import { getSurveysDetail } from '@/api/design';
const searchValue = ref('');
const survey = ref<SurveyItem[]>([]);
const total = ref(0);
const loading = ref(false);
const requestLoading = ref(false);
const finished = ref(false);
const currentSurvey = ref<SurveyItem>();
const requestSingle = ref(true);
async function fetchSingleSurvey(sn: string) {
const res = await getSurveysDetail(sn);
// const res = await getSetting({sn})
// console.log(res);
if (res.data.code === 0) {
currentSurvey.value = res.data.data;
}
}
async function fetchSurveys(form: any) {
if (!requestSingle.value) return;
requestSingle.value = false;
requestLoading.value = true;
const params = {
page: form.page,
per_page: form.pageSize,
group_id: 0,
// project_name: searchValue.value
key_word: form.key_word
};
const res = await getSurveysPage(params);
if (res.data.code === 0) {
survey.value = survey.value.concat(res.data.data);
total.value = res.data.meta.total;
loading.value = false;
// 数据全部加载完成
if (survey.value.length >= total.value) {
finished.value = true;
}
} else {
// Toast()
}
requestLoading.value = false;
requestSingle.value = true;
}
function deleteItem(item: SurveyItem, form: any) {
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.page = 1;
clearSurveys();
await fetchSurveys(form);
})
.catch(() => {
// on cancel
});
}
// 保存为模板
async function saveTemplate(item: SurveyItem) {
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);
}
}
function clearSurveys() {
survey.value = [];
}
export {
fetchSurveys,
loading,
finished,
survey,
total,
searchValue,
deleteItem,
saveTemplate,
currentSurvey,
requestLoading,
fetchSingleSurvey,
clearSurveys
};
import { getSurveysPage, deleteSurveys, saveTemplates } from '@/api/home';
import { ref } from 'vue';
import { showDialog, showConfirmDialog, showFailToast, showToast } from 'vant';
import { getSurveysDetail } from '@/api/design';
import { getQuestionList } from '@/api/survey';
import { questionTypeMap } from '@/utils/question/typeMapping';
const searchValue = ref('');
const survey = ref<SurveyItem[]>([]);
const total = ref(0);
const loading = ref(false);
const requestLoading = ref(false);
const finished = ref(false);
const currentSurvey = ref<SurveyItem>();
const requestSingle = ref(true);
async function fetchSingleSurvey(sn: string) {
const res = await getSurveysDetail(sn);
// const res = await getSetting({sn})
// console.log(res);
if (res.data.code === 0) {
currentSurvey.value = res.data.data;
}
}
async function fetchSurveys(form: any) {
if (!requestSingle.value) return;
requestSingle.value = false;
requestLoading.value = true;
const params = {
page: form.page,
per_page: form.pageSize,
group_id: 0,
// project_name: searchValue.value
key_word: form.key_word
};
const res = await getSurveysPage(params);
if (res.data.code === 0) {
survey.value = survey.value.concat(res.data.data);
total.value = res.data.meta.total;
loading.value = false;
// 数据全部加载完成
if (survey.value.length >= total.value) {
finished.value = true;
}
} else {
// Toast()
}
requestLoading.value = false;
requestSingle.value = true;
}
function deleteItem(item: SurveyItem, form: any) {
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.page = 1;
clearSurveys();
await fetchSurveys(form);
})
.catch(() => {
// on cancel
});
}
// 保存为模板
async function saveTemplate(item: SurveyItem) {
const data = JSON.parse(JSON.stringify(item));
// 如果没有通过校验, 弹出提示窗不进行下一步
if (!(await validateSurvey(data))) {
showDialog({
title: '无法保存模板',
message: '问卷内包含移动端暂未兼容题型/逻辑设置请至PC端编辑后重新保存。'
});
return;
}
const res = await saveTemplates(item.sn, data);
if (res.data.code === 200 || res.data.code === 201) {
showConfirmDialog({
message: '模板保存成功,请前往更多模板页面查看',
showCancelButton: false
});
} else {
showFailToast(res.data);
}
}
function clearSurveys() {
survey.value = [];
}
/**
* 校验问卷是否可以保存为模板
* @param data
*/
async function validateSurvey(survey: SurveyItem): Promise<boolean> {
const { data } = await getQuestionList(survey.sn);
const { questions, logics } = data.data;
const questionValid = questions.every((question: any) => {
if (!questionTypeMap.has(question.question_type)) {
return false;
}
return true;
});
// 2 自动填写, 3 是逻辑配额
const logicValid = logics.every((logic: any) => {
if ([0, 1].includes(logic.skip_type)) {
return true;
}
});
// 判断是否是随机题组/循环题组
const surveyValid = !data.data.survey.group_pages?.length > 0;
const cycleValid = !data.data?.cycle_pages?.length > 0;
return questionValid && logicValid && surveyValid && cycleValid;
}
export {
fetchSurveys,
loading,
finished,
survey,
total,
searchValue,
deleteItem,
saveTemplate,
currentSurvey,
requestLoading,
fetchSingleSurvey,
clearSurveys
};

View File

@@ -37,9 +37,9 @@ onUnmounted(() => {
</script>
<template>
<div class="search">
<search-bar placeholder="请输入关键词" v-if="!disableSearch" :value="''" />
</div>
<!-- <div class="search">-->
<!-- <search-bar placeholder="请输入关键词" v-if="!disableSearch" :value="''" />-->
<!-- </div>-->
<section v-if="currentSurvey" class="survey-container">
<!-- 问卷详情部分 -->
<van-cell class="survey-item">
@@ -103,7 +103,7 @@ onUnmounted(() => {
// width: 100%;
.ai-insight {
background-image: url('@/assets/img/home/item-back.png');
background: linear-gradient(to bottom, rgb(239, 249, 252) 0, white 20%);
background-position: 11% 11%;
.ai-insight-content {

View File

@@ -1,84 +1,120 @@
<template>
<div style="width: 100%">
<!-- 优先去上级传递的数值 -->
<section v-for="analysis in questionAnalysis" :key="analysis.stem" class="mt10">
<!-- {{ analysis }} -->
<!-- 问题标题 -->
<el-tag type="success" size="small">
{{ questionTypeMap.get(analysis.question_type as number) }}
</el-tag>
<el-text>{{ analysis.stem }}</el-text>
<!-- 问题图表部分 -->
<chart-msg
v-if="showChart.includes(analysis.question_type)"
:dimension="analysis.option && analysis.option[0]?.option"
:analysis="analysis"
/>
<!-- 问题表格部分 -->
<yl-table
v-if="getTableData(analysis).length > 0"
class="mt10"
:props="getTableHeadProps(analysis.head, analysis.option)"
:data="getTableData(analysis)"
/>
<section v-else>
<empty-container :error-msg="'本题暂无有效答题数据'" :img-src="emptyImg" />
</section>
</section>
<!-- <section v-else>
<empty-container />
</section> -->
</div>
</template>
<script setup lang="ts">
// 空白容器
import EmptyContainer from '@/views/Survey/components/EmptyContainer.vue';
import { questionTypeMap } from '@/utils/question/typeMapping';
import ChartMsg from '@/components/Analysis/Index.vue';
import { getTableData } from './hooks/pieSeries';
import YlTable from '@/components/YlTable/Index.vue';
import { ref } from 'vue';
import { screenLayout } from '@/hooks/browser/useScreen';
import emptyImg from '@/assets/img/emptyImg.png';
// questionTypeMap 自己去对应
const showChart = ref([1, 2, 5, 106, 9, 10]);
// 接受上级传递的 questionAnalysis 数据
const questionAnalysis = defineModel<any[]>('questionAnalysis');
const { width } = screenLayout();
// 构建表头
const getTableHeadProps = (values: any[], option: any[]): TablePropsType[] => {
const head = [];
if (values && values.length > 0) {
values.forEach((item: any) => {
if (item.key !== 'option') {
head.push({
label: item.title,
prop: item.key,
width: values.length < 4 ? width.value / values.length : 100
});
}
});
}
if (option.length > 0 && option[0].option) {
head.unshift({
label: '选项',
prop: 'option',
width: 150
});
}
return head;
};
</script>
<style scoped lang="scss">
.mt10 {
margin-top: 10px;
}
</style>
<template>
<div style="width: 100%">
<!-- 优先去上级传递的数值 -->
<section v-for="(analysis, index) in questionAnalysis" :key="analysis.stem" class="mt10">
<!-- {{ analysis }} -->
<!-- 问题标题 -->
<div class="" style="line-height: 30px">
<el-tag
type="success"
size="small"
v-if="questionTypeMap.get(analysis.question_type as number)"
>
{{ questionTypeMap.get(analysis.question_type as number) }}
</el-tag>
<el-text class="ml10"
>{{ parentIndex ? parentIndex : index + 1 }}. {{ analysis.stem }}</el-text
>
</div>
<!-- 问题图表部分 -->
<!-- 表格td宽度固定 超出部分滚动-->
<div v-if="questionTypeMap.get(analysis.question_type as number)">
<chart-msg
v-if="showChart.includes(analysis.question_type)"
:dimension="analysis.option && analysis.option[0]?.option"
:analysis="analysis"
/>
<!-- 问题表格部分 -->
<yl-table-h
class="mt10"
v-if="getTableData(analysis).length > 0"
:prop="getTableHeadProps(analysis.head, analysis.option)"
:data="getTableData(analysis)"
></yl-table-h>
<!-- <yl-table-->
<!-- v-if="getTableData(analysis).length > 0"-->
<!-- class="mt10"-->
<!-- :props="getTableHeadProps(analysis.head, analysis.option)"-->
<!-- :data="getTableData(analysis)"-->
<!-- />-->
<section v-else>
<empty-container :error-msg="'本题暂无有效答题数据'" :img-src="emptyImg" />
</section>
</div>
<div v-else>
<div class="empty-text">当前题目结果请至PC端查看更多题型兼容敬请期待</div>
</div>
</section>
<!-- <section v-else>
<empty-container />
</section> -->
</div>
</template>
<script setup lang="ts">
// 空白容器
import EmptyContainer from '@/views/Survey/components/EmptyContainer.vue';
import { questionTypeMap } from '@/utils/question/typeMapping';
import ChartMsg from '@/components/Analysis/Index.vue';
import { getTableData } from './hooks/pieSeries';
import YlTable from '@/components/YlTable/Index.vue';
import YlTableH from '@/components/YlTable/yl-table-h.vue';
import { ref } from 'vue';
import { screenLayout } from '@/hooks/browser/useScreen';
import emptyImg from '@/assets/img/emptyImg.png';
// questionTypeMap 自己去对应
const showChart = ref([1, 2, 5, 106, 9, 10]);
// 接受上级传递的 questionAnalysis 数据
const questionAnalysis = defineModel<any[]>('questionAnalysis');
const parentIndex = defineModel<any>('parentIndex');
const { width } = screenLayout();
// 构建表头
const getTableHeadProps = (values: any[], option: any[]): TablePropsType[] => {
const head = [];
if (values && values.length > 0) {
values.forEach((item: any) => {
if (item.key !== 'option') {
head.push({
label: item.title,
prop: item.key,
width: values.length < 4 ? width.value / values.length : 100,
tooltip: false
});
}
});
}
if (option.length > 0 && option[0].option) {
head.unshift({
label: '选项',
prop: 'option',
width: 150
});
}
return head;
};
</script>
<style scoped lang="scss">
.mt10 {
margin-top: 10px;
}
.ml10 {
margin-left: 10px;
}
.empty-text {
font-size: 11px;
color: #919191;
padding: 20px 10px;
text-align: center;
}
</style>

View File

@@ -21,11 +21,12 @@ export const series = ref({
}
});
export function formatData(data: any, index: number) {
export function formatData(data: any, index: number, isEmpty: boolean = true) {
const _series = JSON.parse(JSON.stringify(series.value));
// 当内容为单选的时候处理方式
if (data.question_type === 1 || data.question_type === 2) {
const { option } = data;
let { option } = data;
_series.data = option.map((item: any) => {
return {
...item,
@@ -34,14 +35,26 @@ export function formatData(data: any, index: number) {
questionItem: { ...data }
};
});
if (isEmpty) {
_series.data = _series.data.filter((item) => item.value != 0);
}
}
if (
data.question_type === 5
|| data.question_type === 9
|| data.question_type === 106
|| data.question_type === 10
data.question_type === 5 ||
data.question_type === 9 ||
data.question_type === 106 ||
data.question_type === 10
) {
const copyData = setDimensionData(data);
let copyData = setDimensionData(data);
// copyData 删除 value 为0 的数据
if (isEmpty) {
copyData = copyData.map((item) => {
return item.filter((item) => item.value !== 0);
});
}
_series.data = copyData[index || 0]?.map((item) => {
return {
...item,
@@ -54,7 +67,7 @@ export function formatData(data: any, index: number) {
return _series;
}
export function getTableData(data: any) {
export function getTableData(data: any, isEmpty = true) {
const analysis = JSON.parse(JSON.stringify(data));
const rows = analysis.option || [];
return rows.map((rowItem: any) => {

View File

@@ -20,7 +20,7 @@ const tabs = computed(() => [
title: '逻辑配额',
props: [
{ prop: 'question_title', label: '题号', width: 80 },
{ prop: 'logic_text', label: '选项', width: 90 },
{ prop: 'text', label: '选项', width: 90 },
{ prop: 'sample_number', label: '样本量', width: 90 },
{ prop: 'percent', label: '进度', width: 90 }
],
@@ -74,8 +74,20 @@ const debug = ref(false);
<yl-table
:data="currentTabs.find((tab) => tab.title === activeTab)?.data"
:props="currentTabs.find((tab) => tab.title === activeTab)?.props"
:emptyText="`此问卷未设置${activeTab}`"
></yl-table>
</section>
</template>
</van-cell>
</template>
<style scoped lang="scss">
.logic-info {
&::after {
content: '';
border: none;
left: 0;
width: 0;
}
}
</style>

View File

@@ -11,7 +11,7 @@ import { aiInsightsConfig } from '@/views/Survey/views/Analysis/hooks/useAnalysi
{{
aiInsightsConfig.info.length !== 0
? aiInsightsConfig.info
: '正在为您分析问卷内容,这个过程可能会多花一点点时间,不过马上就好,稍等哦'
: '正在为您进行AI洞察这次需要多分析一会儿才能确保洞察精准度认真工作的我马上带着结果回来哦'
}}</el-text
>
</section>

View File

@@ -52,9 +52,6 @@ export async function postAnalysis(sn: string) {
clearInterval(aiInsightsConfig.value.timer);
// 获取洞察结果
const { data } = await getAnalysis(sn);
console.log(data.other.overall_conclusion);
console.log(data);
aiAnalysisData.value = data;
aiInsightsConfig.value.message = data.other.overall_conclusion;
@@ -71,7 +68,5 @@ export async function checkAnalysis(sn: string) {
return data;
}
export {aiAnalysisData, currentSn };
export { aiAnalysisData, currentSn };
export { useFetchAnalysis } from '@/hooks/request/useSurvey';

View File

@@ -836,7 +836,9 @@ const publishQuestion = () => {
// 预览
const previewQuestion = () => {
saveAs(() => {
router.push({ name: 'preview', query: { ...route.query } });
setTimeout(() => {
router.push({ name: 'preview', query: { ...route.query } });
}, 500);
});
};

View File

@@ -1,6 +1,7 @@
<template>
<!-- <layout />-->
<div ref="scrollbar" class="preview-container" v-memo="[currentSn]">
<div ref="scrollbar" class="preview-container">
<!-- {{ questionsData }}-->
<!-- <van-nav-bar :title="getDomString(questionsData?.survey?.title)" left-arrow />-->
<!-- 进度条 -->
@@ -49,7 +50,7 @@
<!-- 问题 -->
<!-- eslint-disable-next-line -->
<div class="questions">
<div class="questions" v-if="questions">
<!-- 提前终止和正常完成 -->
<q-last
v-if="page === pages.length + 1"
@@ -568,7 +569,9 @@ const {
translatedText,
isTemplate
} = storeToRefs(questionStore);
questionsData.value = {};
questionStore.fetchQuestions();
// 第一次进入页面清空答案
//初始化页面到第一页
questionsData.value?.questions && clearAnswer(questionsData.value.questions);
@@ -586,13 +589,12 @@ const props = defineProps({
default: false
}
});
// 初始化 isTemplate
isTemplate.value = props.isTemplate ? props.isTemplate : route.query.is_template === '1';
// 如果当前的 sn 和 之前记录的sn不相同的时候那么就要重新请求数据
if (currentSn.value !== route.query.sn) {
questionStore.fetchQuestions();
}
// if (currentSn.value !== route.query.sn) {
// }
// 上一页
async function previous() {
@@ -619,6 +621,10 @@ async function previous() {
// 下一页
async function next(callbackBeforePage) {
// 先清除所有错误
questions.value.map((ques) => {
ques.error = '';
});
// 开始校验答案
startValidate();
// console.log(`click next button`, prevLoading.value || loading.value);
@@ -645,10 +651,27 @@ async function answer(callback, callbackBeforePage) {
if ((questions.value.length || !questionsData.value.questions.length) && !props.isTemplate) {
// 表单验证(当前页)
const errors = questions.value.filter((question) => {
const { config, answer, question_type: questionType, error } = question;
const { config, question_type: questionType, error, answer } = question;
console.log(question.answer);
// let answer = question.answer.value ? question.answer.value : question.answer;
// console.log(answer, questionType, error);
// 单独 处理 图文
if (questionType === 6) {
return;
}
let isError = false;
// 如果问题没有答案还有是必须填空的,就往下处理
// 2025/4/1新增 如果有 error 内容, 同样视为有错误
// if (config.is_required === 0) {
// if (!answer && !answer?.value) {
// return;
// }
// }
console.log(answer, ' isError = true;');
if (!answer || error) {
isError = true;
// 各个问题单独处理
@@ -671,7 +694,12 @@ async function answer(callback, callbackBeforePage) {
// 分类题
question.error = translatedText.value.PleaseCategorizeAllOptions;
} else if (!question.error) {
question.error = translatedText.value.ThisIsARequiredQuestion;
console.log(question, 23);
if (config.is_required === 1) {
question.error = translatedText.value.ThisIsARequiredQuestion;
} else {
isError = false;
}
}
} else if (
answer &&
@@ -683,6 +711,11 @@ async function answer(callback, callbackBeforePage) {
question.error = translatedText.value.PleaseInputAValue;
} else if (answer && questionType === 2) {
// 多选题
// 选项数量
// isError = true;
// question.error = translatedText.value.PleaseSelectAtLeastOneOptions(
// config.min_select ? config.min_select : 0
// );
} else if (answer && questionType === 10) {
// 矩阵多选题
} else if (answer && questionType === 12) {
@@ -697,7 +730,9 @@ async function answer(callback, callbackBeforePage) {
if (Object.keys(answer).length < (+config.min_select || 0)) {
// 选项数量
isError = true;
question.error = translatedText.value.PleaseSelectAtLeastOneOptions(config.min_select);
question.error = translatedText.value.PleaseSelectAtLeastOneOptions(
config.min_select ? config.min_select : 0
);
}
} else if (answer && questionType === 17) {
// 恒定总和题
@@ -716,11 +751,108 @@ async function answer(callback, callbackBeforePage) {
isError = true;
question.error = translatedText.value.PleaseUploadAtLeastOneFiles(config.min_number);
} else if (answer && questionType === 4) {
question.error = '';
if (!answer.value && config.is_required === 0) {
isError = false;
return;
}
if (answer.value === null) {
isError = true;
console.log(123123);
question.error = translatedText.value.ThisIsARequiredQuestion;
return;
}
// // 矩阵填空题
switch (config.text_type) {
// 字母
case 3:
if (
!/^[a-zA-Z·~@#¥%…&*()—\-+={}|《》?:“”【】、;‘’,。`!$^()_<>?:",./;'\\[\]]+$/.test(
answer.value
)
) {
isError = true;
question.error = translatedText.value.PleaseEnterEnglishLetters;
}
break;
// 中文
case 4:
if (
!/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|[·~@#¥%…&*()—\-+={}|《》?:“”【】、;‘’,。`!$^()_<>?:",./;'\\[\]])+$/.test(
answer.value
)
) {
isError = true;
question.error = translatedText.value.PleaseEnterChineseWords;
}
break;
// 邮箱
case 5:
if (
!/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
answer.value
)
) {
isError = true;
question.error = translatedText.value.PleaseEnterACorrectEmail;
}
break;
// 手机号
case 6:
if (!/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(answer.value)) {
isError = true;
question.error = translatedText.value.PleaseEnterACorrectPhone;
}
break;
// 身份证号
case 7:
if (
!/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/.test(
answer.value
)
) {
isError = true;
question.error = translatedText.value.PleaseEnterACorrectID;
}
break;
default:
break;
}
console.log(answer.value?.length, config.text_type, config.min);
// if (![5, 6, 7].includes(config.text_type)) {
if (
answer.value?.length < config.min &&
![1, 2, 5, 6, 7].includes(config.text_type) &&
config.min
) {
isError = true;
question.error = translatedText.value.PleaseEnterMoreThanOneCharacters(config.min);
return;
}
if (
answer.value?.length > config.max &&
![1, 2, 5, 6, 7].includes(config.text_type) &&
config.max
) {
isError = true;
question.error = translatedText.value.PleaseEnterLessCharacters(config.max);
return;
}
// }
if (question.error) isError = true;
} else if (answer && questionType === 8) {
// 矩阵填空题
question.error = '';
if (!answer && config.is_required === 0) {
isError = false;
return false;
}
Object.keys(answer).forEach((key) => {
const value = answer[key];
switch (config.text_type) {
// 字母
case 3:
@@ -729,6 +861,7 @@ async function answer(callback, callbackBeforePage) {
value
)
) {
isError = true;
question.error = translatedText.value.PleaseEnterEnglishLetters;
}
break;
@@ -739,6 +872,7 @@ async function answer(callback, callbackBeforePage) {
value
)
) {
isError = true;
question.error = translatedText.value.PleaseEnterChineseWords;
}
break;
@@ -749,12 +883,14 @@ async function answer(callback, callbackBeforePage) {
value
)
) {
isError = true;
question.error = translatedText.value.PleaseEnterACorrectEmail;
}
break;
// 手机号
case 6:
if (!/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(value)) {
isError = true;
question.error = translatedText.value.PleaseEnterACorrectPhone;
}
break;
@@ -765,14 +901,36 @@ async function answer(callback, callbackBeforePage) {
value
)
) {
isError = true;
question.error = translatedText.value.PleaseEnterACorrectID;
}
break;
default:
break;
}
if (!question.error && value.length < config.min && ![1, 2].includes(config.text_type)) {
question.error = translatedText.value.PleaseEnterMoreThanOneCharacters(config.min);
if (![5, 6, 7].includes(config.text_type)) {
if (
!question.error &&
value.length < config.min &&
config.min &&
![1, 2].includes(config.text_type)
) {
isError = true;
question.error = translatedText.value.PleaseEnterMoreThanOneCharacters(config.min);
return false;
}
if (
!question.error &&
value.length > config.max &&
config.max &&
![1, 2].includes(config.text_type)
) {
isError = true;
question.error = translatedText.value.PleaseEnterLessCharacters(config.max);
return false;
}
}
});
if (question.error) isError = true;
@@ -1315,6 +1473,7 @@ function clearAnswer(questions) {
.preview-container {
overflow: scroll;
height: 100%;
max-height: calc(100vh - var(--sticky-top-height) - 65px - 40px);
//min-height: calc(100vh - 100px);

View File

@@ -34,18 +34,21 @@ const completionValue = ref<string>(answer.value?.value ?? '');
watch(
() => completionValue.value,
(value) => {
console.log(value, 'update');
// 答案校验,生成最终答案
const { isError } = validateCompletion(question.value, question.value.config!, String(value));
console.log(`isError, question`, isError, question.value);
if (isError) {
// 有错误就不更新
question.value.answer = void 0;
return;
}
// if (isError) {
// 有错误就不更新
question.value.answer = void 0;
// return;
// }
const res = generateAnswer(value);
answer.value = res;
question.value!.answer = res;
// answer emit 提交失效
// emit('changeAnswer', res);
}
@@ -56,6 +59,8 @@ watch(
* @param answer {string}
*/
function generateAnswer(answer: string) {
console.log(answer, 'asjd');
return { value: answer };
}
</script>

View File

@@ -3,16 +3,17 @@ import FileUpload from '@/views/Design/components/Questions/FileUpload.vue';
const questionIndex = defineModel<number>('questionIndex', { default: NaN });
const answerIndex = computed(() => question.value.title);
const question = defineModel<question>('question', { default: () => {} });
import { answer } from '@/views/Design/components/Questions/hooks/useFileUploadHooks';
// import { answer } from '@/views/Design/components/Questions/hooks/useFileUploadHooks';
import { computed, watch } from 'vue';
const emit = defineEmits(['changeAnswer']);
watch(answer, () => {
// 暂时先将答案挂到 question,后续需要优化
question.value.answer = answer.value;
// emit('changeAnswer', answer.value);
// console.log(`question`, question.value);
});
// watch(answer, () => {
// // 暂时先将答案挂到 question,后续需要优化
// question.value.answer = answer.value;
// console.log(question.value.answer);
// // emit('changeAnswer', answer.value);
// // console.log(`question`, question.value);
// });
</script>
<template>
<!-- 文件上传题 -->

View File

@@ -1,15 +1,16 @@
<script setup lang="ts">
import TextWithImages from '@/views/Design/components/Questions/TextWithImages.vue';
import { computed } from 'vue';
import { computed, watch } from 'vue';
// 问题
const question = defineModel<question>('question', { default: {} });
// question 序号
const answerIndex = computed(() => question.value?.title ?? 0);
// 答案
const answer = defineModel('answer', { default: {} });
// answer 提供默认值
answer.value = {};
const answer = defineModel('answer');
// setTimeout(() => {
// answer.value = '123';
// }, 300);
</script>
<template>

View File

@@ -2,8 +2,7 @@
<div class="question-conclude-wrapper">
<div class="content">
<div v-if="isTemplate" v-html="survey?.success_end_content" />
<div v-if="code === 20004" v-html="survey?.screening_end_content" />
<div v-else-if="code === 20004" v-html="survey?.screening_end_content" />
<div v-else-if="code === 20011" v-html="survey?.success_end_content" />
<div v-else-if="code === 20016" v-html="survey?.quota_end_content" />
</div>

View File

@@ -6,7 +6,10 @@ import { validateEmail } from '@/views/Survey/views/Preview/components/questions
import { validatePhone } from '@/views/Survey/views/Preview/components/questions/validate/validatePhone';
import { validateIDCard } from '@/views/Survey/views/Preview/components/questions/validate/validateIDCard';
import type { ITranslatedText } from '@/views/Survey/views/Preview/components/questions/validate/validateChineseLetter';
import { validateMinLength,validateMaxLength } from '@/views/Survey/views/Preview/components/questions/validate/validateStringLength';
import {
validateMinLength,
validateMaxLength
} from '@/views/Survey/views/Preview/components/questions/validate/validateStringLength';
/**
* 生成对应的语言文字

View File

@@ -20,7 +20,7 @@ describe('validateEmail 函数测试', () => {
'test123@example.com'
];
validEmails.forEach(email => {
validEmails.forEach((email) => {
const result = validateEmail(email, mockTranslatedText);
expect(result).toBe('');
});
@@ -40,7 +40,7 @@ describe('validateEmail 函数测试', () => {
'test@example.com@example.com'
];
invalidEmails.forEach(email => {
invalidEmails.forEach((email) => {
const result = validateEmail(email, mockTranslatedText);
expect(result).toBe(mockTranslatedText.PleaseEnterACorrectEmail);
});

View File

@@ -19,7 +19,7 @@ describe('validateIDCard 函数测试', () => {
'11010119900307095x'
];
validIDs.forEach(id => {
validIDs.forEach((id) => {
const result = validateIDCard(id, mockTranslatedText);
expect(result).toBe('');
});
@@ -38,7 +38,7 @@ describe('validateIDCard 函数测试', () => {
'12345678901234567' // 格式错误
];
invalidIDs.forEach(id => {
invalidIDs.forEach((id) => {
const result = validateIDCard(id, mockTranslatedText);
expect(result).toBe(mockTranslatedText.PleaseEnterACorrectID);
});

View File

@@ -20,7 +20,7 @@ describe('validatePhone 函数测试', () => {
'008613800138000'
];
validPhones.forEach(phone => {
validPhones.forEach((phone) => {
const result = validatePhone(phone, mockTranslatedText);
expect(result).toBe('');
});
@@ -38,7 +38,7 @@ describe('validatePhone 函数测试', () => {
'00112345678901' // 非中国号码
];
invalidPhones.forEach(phone => {
invalidPhones.forEach((phone) => {
const result = validatePhone(phone, mockTranslatedText);
expect(result).toBe(mockTranslatedText.PleaseEnterACorrectPhone);
});

View File

@@ -20,7 +20,7 @@ describe('validateStringLength 函数测试', () => {
it('当字符串长度大于最小值时,应返回 undefined', () => {
const answer = '测试字符串';
const config: ICompletionConfig = { min: 3, max: 20, text_type: 0 };
const result = validateMinLength(answer, false, config, mockTranslatedText);
expect(result).toBeUndefined();
});
@@ -28,7 +28,7 @@ describe('validateStringLength 函数测试', () => {
it('当字符串长度小于最小值时,应返回错误信息', () => {
const answer = '测试';
const config: ICompletionConfig = { min: 5, max: 20, text_type: 0 };
const result = validateMinLength(answer, false, config, mockTranslatedText);
expect(result).toEqual({
isError: true,
@@ -39,7 +39,7 @@ describe('validateStringLength 函数测试', () => {
it('当 min 为空字符串时,应返回 undefined', () => {
const answer = '';
const config: ICompletionConfig = { min: '', max: 20, text_type: 0 };
const result = validateMinLength(answer, false, config, mockTranslatedText);
expect(result).toBeUndefined();
});
@@ -47,7 +47,7 @@ describe('validateStringLength 函数测试', () => {
it('当已有错误时,应返回 undefined', () => {
const answer = '测试';
const config: ICompletionConfig = { min: 5, max: 20, text_type: 0 };
const result = validateMinLength(answer, true, config, mockTranslatedText);
expect(result).toBeUndefined();
});
@@ -56,10 +56,10 @@ describe('validateStringLength 函数测试', () => {
const answer = '123';
const config1: ICompletionConfig = { min: 5, max: 20, text_type: 1 };
const config2: ICompletionConfig = { min: 5, max: 20, text_type: 2 };
const result1 = validateMinLength(answer, false, config1, mockTranslatedText);
const result2 = validateMinLength(answer, false, config2, mockTranslatedText);
expect(result1).toBeUndefined();
expect(result2).toBeUndefined();
});
@@ -69,7 +69,7 @@ describe('validateStringLength 函数测试', () => {
it('当字符串长度小于最大值时,应返回 undefined', () => {
const answer = '测试字符串';
const config: ICompletionConfig = { min: 3, max: 20, text_type: 0 };
const result = validateMaxLength(answer, false, config, mockTranslatedText);
expect(result).toBeUndefined();
});
@@ -77,7 +77,7 @@ describe('validateStringLength 函数测试', () => {
it('当字符串长度大于等于最大值时,应返回错误信息', () => {
const answer = '这是一个非常长的测试字符串,超过了最大长度限制';
const config: ICompletionConfig = { min: 5, max: 20, text_type: 0 };
const result = validateMaxLength(answer, false, config, mockTranslatedText);
expect(result).toEqual({
isError: true,
@@ -88,7 +88,7 @@ describe('validateStringLength 函数测试', () => {
it('当 max 为空字符串时,应返回 undefined', () => {
const answer = '测试字符串';
const config: ICompletionConfig = { min: 3, max: '', text_type: 0 };
const result = validateMaxLength(answer, false, config, mockTranslatedText);
expect(result).toBeUndefined();
});
@@ -96,7 +96,7 @@ describe('validateStringLength 函数测试', () => {
it('当已有错误时,应返回 undefined', () => {
const answer = '这是一个非常长的测试字符串,超过了最大长度限制';
const config: ICompletionConfig = { min: 5, max: 20, text_type: 0 };
const result = validateMaxLength(answer, true, config, mockTranslatedText);
expect(result).toBeUndefined();
});
@@ -105,10 +105,10 @@ describe('validateStringLength 函数测试', () => {
const answer = '123456789012345678901234567890';
const config1: ICompletionConfig = { min: 5, max: 20, text_type: 1 };
const config2: ICompletionConfig = { min: 5, max: 20, text_type: 2 };
const result1 = validateMaxLength(answer, false, config1, mockTranslatedText);
const result2 = validateMaxLength(answer, false, config2, mockTranslatedText);
expect(result1).toBeUndefined();
expect(result2).toBeUndefined();
});

View File

@@ -106,7 +106,7 @@ function validateMatrixCheckbox(
: translatedText.PleaseSelectLessOptionsPerLine(maxSelect, rowIndex);
}
});
console.log(rows.options!.length, answer.length);
// 检测所有的答案是否正常选择
if (errorMessage.length === 0 && rows.options!.length > answer.length) {
errorMessage = translatedText.PleaseSelectAllRows;

View File

@@ -19,7 +19,7 @@ function validateMultiSelectQuestion(
// 多选题
// 如果答案数量小于最小选择的数量
if (answer.length < Number(config.min_select)) {
if (answer.length < Number(config.min_select) && config.min_select) {
// 选项数量
// let options: questionOptionType[] = [];
// 疑问脸
@@ -38,7 +38,7 @@ function validateMultiSelectQuestion(
} else {
errorMessage = '';
}
} else if (answer.length > Number(config.max_select)) {
} else if (answer.length > Number(config.max_select) && config.max_select) {
errorMessage = translatedText.PleaseSelectNoMoreOptions(config.max_select);
} else if (answer.findIndex((value) => !value) !== -1) {
errorMessage = translatedText.PleaseInputAValue;

View File

@@ -22,7 +22,7 @@ export function validateMinLength(
// 如果包含相应的 text_type 也不处理1是整数2是小数
if ([1, 2].includes(config.text_type)) return;
if (answer.length > Number(config.min)) return;
if (answer.length >= Number(config.min)) return;
return {
isError: true,
errorMessage: translatedText.PleaseEnterMoreCharacters(config.min)

View File

@@ -102,6 +102,10 @@ export const language = {
en: 'Please enter a valid ID card number.',
zh: '请输入正确的身份证号。'
},
PleaseEnterMoreThanOneCharacters: {
en: (count) => `Please enter more than ${count} character${count > 1 ? 's' : ''}.`,
zh: (count) => `请输入大于${count}个字符。`
},
PleaseEnterLessCharacters: {
en: (count) => `Please enter less than ${count} character${count > 1 ? 's' : ''}.`,
zh: (count) => `请输入小于${count}个字符。`

View File

@@ -1,7 +1,7 @@
import { test, spec } from "vitest";
import { getLanguage } from "../language";
import { test, spec } from 'vitest';
import { getLanguage } from '../language';
test("检测 language 列表", () => {
const res = getLanguage(['zh'])
test('检测 language 列表', () => {
const res = getLanguage(['zh']);
console.log(res);
});
});

View File

@@ -143,7 +143,7 @@ function downLoadImg() {
const { title, url } = publishInfo.value.download_url;
if (appBridge.isInReactNative()) {
appBridge.save2Album(url, () => {
showToast('二维码下载成功,请打系统相册查看');
showToast('二维码下载成功,请打系统相册查看');
});
} else {
const link = document.createElement('a');

View File

@@ -11,7 +11,7 @@ import legacy from '@vitejs/plugin-legacy';
// shift + alt 快速定位到对应组件
import { codeInspectorPlugin } from 'code-inspector-plugin';
// 导入 dev tools
import vueDevTools from 'vite-plugin-vue-devtools'
import vueDevTools from 'vite-plugin-vue-devtools';
export default defineConfig(({ mode }) => {
// 接收 mode 参数
@@ -24,6 +24,7 @@ export default defineConfig(({ mode }) => {
return {
// 必须 return 配置对象
server: {
allowedHosts: ['yiligpt.x.digitalyili.com'],
host: '0.0.0.0',
port: 3000,
proxy: {