Compare commits

..

103 Commits

Author SHA1 Message Date
dayou
fad2f39cf3
Fix/pinia v2 (#453)
* fix: 锁版本

* fix: 锁版本
2024-11-29 18:19:10 +08:00
dayou
f01465aa6c fix: 解决冲突 2024-10-31 21:40:32 +08:00
Liang-Yaxin
12f19559a9
[Feature]:我的空间新增分组管理 (#439)
* fix: 问卷列表更多按钮图标优化

* feat: 我的空间新增分组管理

* feat: 我的空间新增分组管理

* feat: 我的空间新增分组管理

* feat: 我的空间新增分组管理

* fix: 修改单元测试

* fix: 修改单元测试

* fix: 修改单元测试

* fix: 修改单元测试

* fix: 修改我的空间验收问题

* fix; 修改我的空间验收问题

* fix: 修改二次验收相关问题

---------

Co-authored-by: dayou <853094838@qq.com>
2024-10-31 21:39:08 +08:00
Jiangchunfu
20f01768cf
空间管理样式调整 (#441)
* feat: 空间编辑保存名字同步

* feat: 团队空间空数据隐藏分页器

* feat: 问卷列表添加协作者输入为空时不发接口请求

* feat: 管理团队文字对齐向左

* feat: 团队空间编辑保存名字同步

* feat: 隐藏校验框border

* feat: 问卷列表添加协作者为空时不触发请求

* feat: 隐藏协作管理搜索框校验边框样式

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-10-31 18:00:03 +08:00
Liang-Yaxin
f6202d4f91
feat: 新增题目优化 (#443) 2024-10-31 17:58:38 +08:00
Liang-Yaxin
c93515d2bd
feat: 编辑态优化 (#444) 2024-10-31 17:58:17 +08:00
dayou
6c9ac33ea5
fix: 修复c端页面报错 (#448) 2024-10-30 17:56:30 +08:00
dayou
dffc70c005
fix: 修复依赖版本 (#445)
* fix: 修复依赖版本

* fix: nanoid version

* fix: sass锁定1.79.6的版本

* nanoid reset

* fix: modern-compiler
2024-10-28 21:20:56 +08:00
luch1994
d4bdedb325 feat: 升级docker的版本 2024-10-08 21:28:07 +08:00
Donald Yang
3d4171a7f7
feat:自动保存优化 (#435) 2024-10-08 20:47:33 +08:00
luch
26d92b35ea
feat: 把report迁移到src下 (#440) 2024-10-08 16:26:33 +08:00
sudoooooo
6056a0afde feat: 优化readme 2024-10-08 15:45:08 +08:00
luch
ea8e901d2b
feat: 修改补充单测 (#437) 2024-09-30 18:23:28 +08:00
luch
351f18f255
[Feature] 新增暂停功能以及多表字段升级 (#434)
* feat:新增暂停功能  (#416)

* feat:新增暂停功能

* feat: 修改状态相关功能 (#433)

Co-authored-by: luchunhui <luchunhui@didiglobal.com>

* fix: 修改刷数据逻辑错误和环境变量问题

* fix: 解决lint问题

---------

Co-authored-by: chaorenluo <1243357953@qq.com>
Co-authored-by: luchunhui <luchunhui@didiglobal.com>
2024-09-27 19:52:01 +08:00
sudoooooo
054095e499 feat: 优化内容 2024-09-27 18:24:54 +08:00
Jiangchunfu
81df0eae05
feat: 结果页新增跳转设置 (#432)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-09-27 18:14:32 +08:00
sudoooooo
0a27554bb6 feat: 升级README 2024-09-27 17:21:50 +08:00
sudoooooo
5dfd3d941f feat: Create CODE_OF_CONDUCT.md 2024-09-27 15:16:55 +08:00
sudoooooo
60f96386a9 fix: 修复文档链接 2024-09-24 20:55:41 +08:00
sudoooooo
7f832ce885 fix: 优化配置内容 2024-09-24 15:48:39 +08:00
Jiangchunfu
47ea148866
feat: 皮肤设置2.0 (#426)
* fix: 修复windows上传文件路径反斜杠问题

* feat: 补全服务端skinConf定义

* feat: web端皮肤背景设置2.0

* feat: 删除console.log

* feat: 皮肤设置内容结果增加背景设置以及应用皮肤设置方法抽离

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
Co-authored-by: sudoooooo <zjbbabybaby@gmail.com>
2024-09-24 14:09:37 +08:00
sudoooooo
129a803e9a fix: 优化类型校验 2024-09-24 12:13:44 +08:00
sudoooooo
bc0597efe0 fix: 修改api 2024-09-23 15:20:13 +08:00
sudoooooo
365bea25e1 fix: 修复投票题问题 2024-09-23 14:25:09 +08:00
dayou
3d5f04b4a8
fix: 修复最少最多选择 (#412)
* fix: 修复最少最多选择

* fix: 优化获取协作下拉框在协作弹窗出现时调用

* fix: 回退最多最少选择

* perl: 代码优化以及fix lint

---------

Co-authored-by: sudoooooo <zjbbabybaby@gmail.com>
2024-09-23 12:33:30 +08:00
Jiangchunfu
f3ebc11b3f
fix: 修复做题页面提交reload页面 (#419)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
Co-authored-by: sudoooooo <zjbbabybaby@gmail.com>
2024-09-23 12:29:50 +08:00
sudoooooo
d5669352ed fix: 优化schema初始化管理 2024-09-23 11:47:11 +08:00
sudoooooo
0b686ab4ac fix: 优化代码内容 2024-09-22 12:38:42 +08:00
luch
54a86e1501
feat: 修改导出和登录状态检测代码CR问题 (#431) 2024-09-22 01:42:27 +08:00
sudoooooo
cdb8b6532a fix: 修复写法问题、C端组件问题 2024-09-22 01:41:32 +08:00
dayou
56c37fce3c
fix: 断点续答and编辑检测代码cr优化 (#428)
* fix: 优化代码以及去掉无用字段

* fix: 断点续答验收问题优化

* fix: 优化字段

* fix: lint

* fix: 编辑检测相关问题优化

* fix: lint

* fix: 第二批cr优化

* fix: 去掉无用代码

* fix: 调整session守卫位置

* fix: 文件大小写
2024-09-20 13:26:59 +08:00
sudoooooo
43b20b1be6 fix: 移除有问题的功能 2024-09-14 13:50:51 +08:00
dayou
b749cfa6f6
【Feature】:北大实践课作业 (#424)
* 【北大开源实践】增加数据导出功能 (#294)

* feat:添加了一个文件数据导出的功能和相应前端页面

* fix lint

* fix conflict

---------

Co-authored-by: dayou <853094838@qq.com>

* fix: components.d.ts文件ignore

* feat: Update README_EN.md

* feat: Update README.md

* feat:新增预览功能 (#257)

* feat:问卷预览功能
* feat:修复样式问题

* fix: 优化预览展示

* refactor: 重构vue3组合式API写法 (#265)

* feat: 抽离题型枚举 (#272)

* feat: 抽离题型枚举

* fix: 投放的链接加时间戳去掉ifream缓存

* feat: serve端的node engines

* feat: 权限接口请求优化以及修复其他问题 (#290)

* feat: c端路由改造 (#296)

* 【北大开源实践】增加数据导出功能 (#294)

* feat:添加了一个文件数据导出的功能和相应前端页面

* fix lint

* fix conflict

---------

Co-authored-by: dayou <853094838@qq.com>

* fix: 删除components.d.ts文件

* 【北大开源实践】- 问卷断点续答 - 前端 (#282)

* feat:增加断点续答功能

* feat:增加断点续答功能

* fix: 同步代码并且解决冲突

---------

Co-authored-by: dayou <853094838@qq.com>

* fix: 删除components.d.ts文件最终

* 【北大开源实践】-选项限制 (#284)

* format: 代码格式化 (#160)

* feat: 选项限制

* fix: 同步代码并解决冲突

* fix conflict

* fix conflict

* fix lint

* fix server lint

---------

Co-authored-by: dayou <853094838@qq.com>
Co-authored-by: XiaoYuan <2521510174@qq.com>

* feat: 登录失效检测 & 协作冲突检测 (#287)

Co-authored-by: Liuxinyi <liuxy0406@163.com>
Co-authored-by: dayou <853094838@qq.com>

* fix: peking分支同步develop并解决冲突

* fix: 修正颜色不统一 (#338)

* fix: 修正颜色不统一

* fix: 删除server下的lock文件

* 编辑冲突检测 (#351)

* perl: 选项配额优化

* fix: pinia改写

* feat: 完善北大课程相关的内容

* fix: 修复断点续答以及样式问题 (#420)

* feat: 修改readme

* [Feature]: 密码复杂度检测 (#407)

* feat: 密码复杂度检测

* chore: 改为服务端校验

* feat: 优化展示

* fix:修复编辑页在不同element版本下表现不一致问题 (#406)

* fix: 通过声明element最低版本来确定tab样式表现

* fix lint

* feat(选项设置扩展):选择类题型增加选项排列配置 (#403)

* build: add optimizeDeps packages

* feat(选项设置扩展):选择类题型增加选项排列配置

* feat(选项设置扩展): 验收问题修复

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>

* fix: 删除多余内容

* feat: 优化登录窗口

* fix: 修复断点续答以及样式问题

fix: 修复选项引用验收bug

fix: 修复断点续答问题

fix: 修复断点续答

fix: ignore

fix: 修复投票题默认值

fix: 优化断点续答逻辑

fix: 选中图标适应高度

fix: 回退最大最小选择

fix: 修复断点续答

fix: 修复elswitch不更新问题

fix: 修复访问密码更新不生效问题

fix: 修复样式

fix: 修复多选题最大最小限制

fix: 优化断点续答问题

修复多选题命中最多选择后无法取消问题

fix: 修复服务端的富文本解析

fix:  lint

fix: min error

fix: 修复最少最多选择

fix: 修复投票问卷的最少最多选择

fix: 兼容断点续答情况下选项配额为0的情况

fix: 兼容断点续答情况下选项配额为0的情况

fix: 兼容单选题的断点续答下的选项配额

fix: 修复添加选项问题

fix: 前端提示服务的配额已满

fix: 更新填写的过程中配额减少情况

---------

Co-authored-by: sudoooooo <zjbbabybaby@gmail.com>
Co-authored-by: Stahsf <30379566+50431040@users.noreply.github.com>
Co-authored-by: Jiangchunfu <mrj_kevin@163.com>
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>

* feat: 修改验收问题 (#421)

* fix lint

---------

Co-authored-by: Oseast <162945153+Oseast@users.noreply.github.com>
Co-authored-by: sudoooooo <zjbbabybaby@gmail.com>
Co-authored-by: chaorenluo <1243357953@qq.com>
Co-authored-by: Realabiha <48506355+Realabiha@users.noreply.github.com>
Co-authored-by: shiyiting763 <70299297+shiyiting763@users.noreply.github.com>
Co-authored-by: yiyeah <68832436+yiyeah@users.noreply.github.com>
Co-authored-by: XiaoYuan <2521510174@qq.com>
Co-authored-by: Xinyi Liu <74805961+colmon46@users.noreply.github.com>
Co-authored-by: Liuxinyi <liuxy0406@163.com>
Co-authored-by: nil <wangweiguo2013@icloud.com>
Co-authored-by: 王晓聪 <wang86976110@126.com>
Co-authored-by: taoshuang <taoshuang@didiglobal.com>
Co-authored-by: luch1994 <1097650398@qq.com>
Co-authored-by: Stahsf <30379566+50431040@users.noreply.github.com>
Co-authored-by: Jiangchunfu <mrj_kevin@163.com>
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
Co-authored-by: luch <32321690+luch1994@users.noreply.github.com>
2024-09-12 22:10:18 +08:00
Jiangchunfu
43001a12c7
fix: 修复富文本编辑器上传图片 (#410)
* fix: 修复题目标题插入图片异常问题

* fix: 修改事件监听顺序,避免编辑图片百分比时重新渲染工具条而找不到点击的dom

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-09-06 15:51:12 +08:00
Stahsf
d3c2180ac8
test: 密码检测用例 (#411)
* feat: 密码复杂度检测

* chore: 改为服务端校验

* test: 修改用例

* test: 添加getPasswordStrength测试用例
2024-09-06 15:37:03 +08:00
sudoooooo
63e16e1694 feat: 优化登录窗口 2024-09-03 17:55:59 +08:00
sudoooooo
c6cc0d22e5 fix: 删除多余内容 2024-09-03 16:47:33 +08:00
Jiangchunfu
949a989dcf
feat(选项设置扩展):选择类题型增加选项排列配置 (#403)
* build: add optimizeDeps packages

* feat(选项设置扩展):选择类题型增加选项排列配置

* feat(选项设置扩展): 验收问题修复

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-09-03 15:54:42 +08:00
dayou
9427e0efe5
fix:修复编辑页在不同element版本下表现不一致问题 (#406)
* fix: 通过声明element最低版本来确定tab样式表现

* fix lint
2024-09-02 17:39:47 +08:00
sudoooooo
70c236c879 feat: 优化展示 2024-09-02 17:36:18 +08:00
Stahsf
3d31245ae5
[Feature]: 密码复杂度检测 (#407)
* feat: 密码复杂度检测

* chore: 改为服务端校验
2024-09-02 16:58:53 +08:00
sudoooooo
98fc21995a feat: 修改readme 2024-08-30 12:01:39 +08:00
sudoooooo
f6e3778a2d feat: 优化换行 2024-08-14 21:32:06 +08:00
dayou
6775a9df5e
fix: 删除分页判断是否存在题目的逻辑关联 (#402) 2024-08-14 21:30:16 +08:00
Liang-Yaxin
3cb843e493
fix: 问卷列表更多按钮图标优化 (#401) 2024-08-14 21:12:43 +08:00
dayou
bc3ce31c9e
feat: 跳转逻辑稳定版 (#399)
* feat: 跳转逻辑 (#388)

* fix: 跳转逻辑优化 (#397)

* fix: 跳转逻辑优化

* fix: processJumpSkip逻辑放在题目组件中
2024-08-14 17:59:51 +08:00
sudoooooo
3e7f0cac90 fix: 修复高级设置迁移后无法交互问题 2024-08-14 17:28:59 +08:00
dayou
f3b8ab278a
fix: 更新iconfont链接 (#398) 2024-08-14 10:10:29 +08:00
sudoooooo
013f9ac811 feat: 高级设置组件迁移 2024-08-13 23:44:19 +08:00
sudoooooo
9e07e8330a feat: 升级iconfont 2024-08-13 11:46:59 +08:00
sudoooooo
8950073141 fix: 新建问卷重置计数 2024-08-12 23:10:21 +08:00
Jiangchunfu
4d580bb789
feat: 整卷增加基础配置:必填、显示类型、显示序号、显示分割线 (#391)
* feat: 增加整卷配置功能

* fix: 限制单题修改配置时只对基础配置进行更新全局基础配置操作

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-08-12 21:39:19 +08:00
Jiangchunfu
f73bfb0ab3
feat: C端增加重新填写入口, #182 (#392)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-08-12 19:43:55 +08:00
sudoooooo
8109d350e4 feat: 优化readme 2024-08-12 19:42:01 +08:00
sudoooooo
b233023bb3 fix: 连续添加题目频繁触发事件导致页面卡顿 2024-08-12 16:08:57 +08:00
sudoooooo
fd7cc2ea96 feat: action不处理format 2024-08-12 14:09:29 +08:00
Jiangchunfu
724535a735
refactor: 题目删除和逻辑关联优化,#182 (#393)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-08-12 14:06:38 +08:00
sudoooooo
b5c7ec3008 fix: 皮肤设置tab问题 2024-08-07 22:09:55 +08:00
sudoooooo
c5698ad631 fix: 修复白名单切换和空间成员名字问题 2024-08-07 22:07:00 +08:00
chaorenluo
6fb337633c
fix:修复C端白名单验证弹框不出现的问题 (#389) 2024-08-07 18:33:23 +08:00
Jiangchunfu
42b8d74ead
refactor: 设置器加载统一,代码优化 #269 (#383)
* feat: 小功能建设(4)

* refactor: 设置器加载统一,代码优化 #269

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-08-07 14:53:14 +08:00
sudoooooo
d8e76dc2e6 feat: 修改Readme 2024-08-06 23:46:21 +08:00
sudoooooo
4f2cd4ca47 feat: 修改字段 2024-08-06 19:59:35 +08:00
sudoooooo
82c98ec1f5 feat: 优化分页器结构 2024-08-06 19:33:11 +08:00
chaorenluo
fbc654f21b
feat:新增分页功能 (#382)
* feat:新增分页功能

* 修复type-check检查

* fix: server  type-check

* fix:修改服务端测试用例

* fix:修复分页bug
2024-08-06 17:30:12 +08:00
Jiangchunfu
9d89a1ceca fix: 修复题目未聚焦时拖拽按钮失效问题 (#375)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-08-04 12:41:42 +08:00
Jiangchunfu
517906f77f
feat: 小功能建设(4) (#379) 2024-08-04 12:38:28 +08:00
sudoooooo
681e8fa3ae fix: 修复字段difTime->diffTime 2024-08-04 12:21:08 +08:00
Jiangchunfu
4d5c3eb15d
小功能优化8 (#371)
* refactor: 去除重复元素

* feat: edit 布局优化

* style: 题目大纲滚动时固定tab-header

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-07-31 17:21:54 +08:00
离谱
9a35de7e36
fix(Navbar):修复了短标题hover空白处会触发指示框的bug (#365)
fix(Navbar):修复了短标题hover空白处会触发指示框的bug
2024-07-30 15:27:49 +08:00
dayou
c4b730c8af
fix: 修复提交设置 (#370) 2024-07-30 14:51:33 +08:00
Jiangchunfu
8ea8869ca7 fix: 修复新增同类型题目设置值不变问题 (#359)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-07-23 15:43:10 +08:00
dayou
7e5a8ae5c1
fix: 修复设置器不更新问题 (#361) 2024-07-23 15:39:43 +08:00
sudoooooo
cf495b60d1 Merge branch 'feature/pinia' into develop
# Conflicts:
#	web/src/management/components/LeftMenu.vue
#	web/src/management/pages/edit/components/ModuleNavbar.vue
#	web/src/management/pages/edit/modules/contentModule/PublishPanel.vue
#	web/src/management/pages/edit/modules/contentModule/SavePanel.vue
#	web/src/management/pages/edit/modules/resultModule/CatalogPanel.vue
#	web/src/management/pages/edit/modules/settingModule/SettingPanel.vue
#	web/src/management/pages/list/components/BaseList.vue
#	web/src/management/pages/list/components/SpaceList.vue
#	web/src/management/pages/list/components/SpaceModify.vue
#	web/src/management/pages/list/index.vue
#	web/src/management/router/index.ts
#	web/src/management/store/edit/getters.js
#	web/src/management/store/list/index.js
#	web/src/management/utils/index.js
#	web/src/materials/questions/widgets/BinaryChoiceModule/meta.js
#	web/src/materials/questions/widgets/CheckboxModule/meta.js
#	web/src/materials/questions/widgets/RadioModule/meta.js
#	web/src/materials/questions/widgets/VoteModule/meta.js
#	web/src/render/pages/RenderPage.vue
#	web/src/render/store/actions.js
#	web/src/render/store/mutations.js
#	web/src/render/store/state.js
2024-07-23 14:14:52 +08:00
sudoooooo
fb57eaaba7 feat: 优化结构 2024-07-22 20:37:11 +08:00
sudoooooo
b494bd6174 feat: 优化编辑页结构 2024-07-22 20:27:12 +08:00
sudoooooo
310fe0d325 feat: 优化编辑页模块和配置结构 2024-07-22 17:33:59 +08:00
Stahsf
ba418c5cd7
feat: 白名单功能-服务端 (#357) 2024-07-20 14:11:19 +08:00
chaorenluo
9596cd07a1
前端新增白名单功能 (#356) 2024-07-19 22:45:40 +08:00
sudoooooo
2ed5b64b18 feat: 优化图片尺寸用于移动端 2024-07-18 21:07:25 +08:00
nil
492e0055f0
feat: 题目与选项支持图片 (#291)
* feat: 题目与选项支持图片

* fix: 修复使用本地存储时文件访问路径不正确的问题

* fix: 图片编辑表单无法输入

* chore: 添加上传文件夹到gitignore

* fix: 两个#app的问题
2024-07-18 10:21:09 +08:00
sudoooooo
93938702fe feat: 更新readme和docker tag 2024-07-17 23:12:17 +08:00
sudoooooo
e8907ca4fb fix: format报错 2024-07-16 11:33:16 +08:00
Jiangchunfu
2b32850046
feat: 题型硬编码优化 (#343)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-07-16 11:25:08 +08:00
sudoooooo
da1749fb53 feat: 代码format、升级ts和lint 2024-07-15 22:38:14 +08:00
Ken
5bc5eb8719
feat: 空间列表分页和搜索及数据列表优化 (#344)
1、fix: 修复因滚动条宽度影响皮肤标签的问题
2、feat: 空间列表分页和搜索及数据列表样式优化
3、feat: 对部分代码进行了优化
2024-07-15 18:30:50 +08:00
sudoooooo
3227a799f9 fix: 移除选项默认选中 2024-07-15 18:00:07 +08:00
Jiangchunfu
8740685a4d
feat: 功能18优化 (#342)
题目标题编辑自动focus
2024-07-15 17:50:00 +08:00
sudoooooo
5c3915a74d fix: 空间列表高度 2024-07-15 12:11:57 +08:00
ysansan
b1958ec8ff
问卷编辑页面题型选择tab、团队空间列表按钮优化 (#337)
* feat:问卷编辑页面题型选择tab优化

* feat: 团队空间列表按钮优化
2024-07-15 11:24:51 +08:00
sudoooooo
bc39e9933d fix: 避免element-plus提示sass语法 2024-07-15 11:21:35 +08:00
sudoooooo
36dd5a4f2d feat: 增加csdn和x 2024-07-12 16:49:40 +08:00
sudoooooo
a101878313 feat: 优化协作管理路径 2024-07-10 18:36:54 +08:00
Ken
32e43b8260
feat: 协作者管理优化和皮肤标签样式调整 (#333)
* feat: 皮肤设置内主题分类标签样式调整

* feat: 协作者管理优化
2024-07-10 18:06:39 +08:00
Jiangchunfu
9afb23c08e
feat(小功能建设): 15团队空间问卷列表页优化 (#334) 2024-07-10 16:20:31 +08:00
sudoooooo
eaa1abda82 fix: sass1.77.7规则升级warning 2024-07-10 16:07:29 +08:00
hiStephen
6431cc3210
feat: echarts按需引入 (#332) 2024-07-10 15:38:08 +08:00
sudoooooo
122f584cad fix: 修复预览页logo展示问题 2024-07-09 15:23:02 +08:00
Jiangchunfu
f45cf7982f
小功能优化 (#329)
* feat(换肤设置优化): 边距的颜色优化成背景色一致

* feat: 问卷设置优化
2024-07-09 14:17:44 +08:00
Ken
2f0736fd95
feat: 移动端预览优化 (#326)
* feat: 移动端预览优化

* feat: C端底部logo优化
2024-07-09 12:12:13 +08:00
sudoooooo
6c72344204 fix: 修复nginx启动render页空白问题 2024-07-08 21:06:30 +08:00
sudoooooo
61fd6e09af fix: 优化依赖项 2024-07-01 14:14:07 +08:00
若川
349b4dad8c
fix: 🐛 修复引入 import lodash cloneDeep debounce 错误 改为 lodash-es 了 (#322) 2024-07-01 14:08:57 +08:00
54 changed files with 1641 additions and 308 deletions

View File

@ -1,5 +1,5 @@
# 镜像集成 # 镜像集成
FROM node:18-slim FROM node:18
# 设置工作区间 # 设置工作区间
WORKDIR /xiaoju-survey WORKDIR /xiaoju-survey

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright (C) 2023 Beijing Didi Infinity Technology and Development Co.,Ltd. All rights reserved. Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -161,7 +161,7 @@ npm run local
#### 1.Configure Database #### 1.Configure Database
> The project uses MongoDB: [MongoDB Guide](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E5%AE%89%E8%A3%85) > The project uses MongoDB: [MongoDB Guide](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93)
Configure the database, check MongoDB configuration. Configure the database, check MongoDB configuration.

View File

@ -29,6 +29,7 @@ import { SurveyHistory } from './models/surveyHistory.entity';
import { ResponseSchema } from './models/responseSchema.entity'; import { ResponseSchema } from './models/responseSchema.entity';
import { Counter } from './models/counter.entity'; import { Counter } from './models/counter.entity';
import { SurveyResponse } from './models/surveyResponse.entity'; import { SurveyResponse } from './models/surveyResponse.entity';
import { SurveyGroup } from './models/surveyGroup.entity';
import { ClientEncrypt } from './models/clientEncrypt.entity'; import { ClientEncrypt } from './models/clientEncrypt.entity';
import { Word } from './models/word.entity'; import { Word } from './models/word.entity';
import { MessagePushingTask } from './models/messagePushingTask.entity'; import { MessagePushingTask } from './models/messagePushingTask.entity';
@ -78,6 +79,7 @@ import { Logger } from './logger';
SurveyConf, SurveyConf,
SurveyHistory, SurveyHistory,
SurveyResponse, SurveyResponse,
SurveyGroup,
Counter, Counter,
ResponseSchema, ResponseSchema,
ClientEncrypt, ClientEncrypt,

View File

@ -88,10 +88,17 @@ export interface MsgContent {
msg_9004: string; msg_9004: string;
} }
export interface JumpConfig {
type: string;
link: string;
buttonText?: string;
}
export interface SubmitConf { export interface SubmitConf {
submitTitle: string; submitTitle: string;
confirmAgain: ConfirmAgain; confirmAgain: ConfirmAgain;
msgContent: MsgContent; msgContent: MsgContent;
jumpConfig?: JumpConfig;
} }
// 白名单类型 // 白名单类型

View File

@ -0,0 +1,11 @@
import { Entity, Column } from 'typeorm';
import { BaseEntity } from './base.entity';
@Entity({ name: 'surveyGroup' })
export class SurveyGroup extends BaseEntity {
@Column()
ownerId: string;
@Column()
name: string;
}

View File

@ -37,6 +37,9 @@ export class SurveyMeta extends BaseEntity {
@Column() @Column()
workspaceId: string; workspaceId: string;
@Column()
groupId: string;
@Column() @Column()
curStatus: { curStatus: {
status: RECORD_STATUS; status: RECORD_STATUS;

View File

@ -275,6 +275,11 @@ describe('DataStatisticController', () => {
again_text: '确认要提交吗?', again_text: '确认要提交吗?',
}, },
link: '', link: '',
jumpConfig: {
type: 'link',
link: '',
buttonText: '',
},
}, },
logicConf: { logicConf: {
showLogicConf: [], showLogicConf: [],

View File

@ -71,6 +71,11 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
is_again: true, is_again: true,
again_text: '确认要提交吗?', again_text: '确认要提交吗?',
}, },
jumpConfig: {
type: 'link',
link: '',
buttonText: '',
},
}, },
dataConf: { dataConf: {
dataList: [ dataList: [
@ -365,6 +370,11 @@ export const mockResponseSchema: ResponseSchema = {
is_again: true, is_again: true,
again_text: '确认要提交吗?', again_text: '确认要提交吗?',
}, },
jumpConfig: {
type: 'link',
link: '',
buttonText: '',
},
}, },
dataConf: { dataConf: {
dataList: [ dataList: [

View File

@ -0,0 +1,132 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SurveyGroupController } from '../controllers/surveyGroup.controller';
import { SurveyGroupService } from '../services/surveyGroup.service';
import { SurveyMetaService } from '../services/surveyMeta.service';
import { HttpException } from 'src/exceptions/httpException';
import { ObjectId } from 'mongodb';
import { Logger } from 'src/logger';
jest.mock('src/guards/authentication.guard');
describe('SurveyGroupController', () => {
let controller: SurveyGroupController;
let service: SurveyGroupService;
const mockService = {
create: jest.fn(),
findAll: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SurveyGroupController],
providers: [
{
provide: SurveyMetaService,
useValue: {
countSurveyMetaByGroupId: jest.fn().mockResolvedValue(0),
},
},
{
provide: SurveyGroupService,
useValue: mockService,
},
{
provide: Logger,
useValue: {
error: jest.fn(),
info: jest.fn(),
},
},
],
}).compile();
controller = module.get<SurveyGroupController>(SurveyGroupController);
service = module.get<SurveyGroupService>(SurveyGroupService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('create', () => {
it('should create a survey group', async () => {
const result = {
_id: new ObjectId(),
name: 'Test Group',
ownerId: '123',
createdAt: new Date(),
updatedAt: new Date(),
}; // 确保这里返回的对象结构符合预期
jest.spyOn(service, 'create').mockResolvedValue(result);
// 创建模拟的请求对象
const req = {
user: {
_id: '123', // 模拟的用户ID
},
};
expect(await controller.create({ name: 'Test Group' }, req)).toEqual({
code: 200,
data: {
id: result._id,
},
});
expect(service.create).toHaveBeenCalledWith({
name: 'Test Group',
ownerId: req.user._id.toString(), // 这里用模拟的 req.user._id
});
});
});
describe('findAll', () => {
it('should return a list of survey groups', async () => {
const result = { total: 0, notTotal: 0, list: [], allList: [] };
jest.spyOn(service, 'findAll').mockResolvedValue(result);
const mockReq = { user: { _id: new ObjectId() } };
const mockQue = { curPage: 1, pageSize: 10, name: '' };
const userId = mockReq.user._id.toString();
expect(await controller.findAll(mockReq, mockQue)).toEqual({
code: 200,
data: result,
});
expect(service.findAll).toHaveBeenCalledWith(userId, '', 0, 10);
});
});
describe('update', () => {
it('should update a survey group', async () => {
const updatedFields = { name: 'xxx' };
const updatedResult = { raw: 'xxx', generatedMaps: [] };
const id = '1';
jest.spyOn(service, 'update').mockResolvedValue(updatedResult);
expect(await controller.updateOne(id, updatedFields)).toEqual({
code: 200,
ret: updatedResult,
});
expect(service.update).toHaveBeenCalledWith(id, updatedFields);
});
it('should throw error on invalid parameter', async () => {
const id = '1';
const invalidFields: any = {};
await expect(controller.updateOne(id, invalidFields)).rejects.toThrow(
HttpException,
);
});
});
describe('remove', () => {
it('should remove a survey group', async () => {
const id = '1';
jest.spyOn(service, 'remove').mockResolvedValue(undefined);
expect(await controller.remove(id)).toEqual({ code: 200 });
expect(service.remove).toHaveBeenCalledWith(id);
});
});
});

View File

@ -0,0 +1,102 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SurveyGroupService } from '../services/surveyGroup.service';
import { SurveyGroup } from 'src/models/surveyGroup.entity';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { getRepositoryToken } from '@nestjs/typeorm';
describe('SurveyGroupService', () => {
let service: SurveyGroupService;
const mockSurveyGroupRepository = {
create: jest.fn(),
save: jest.fn(),
findAndCount: jest.fn(),
find: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
const mockSurveyMetaRepository = {
updateMany: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SurveyGroupService,
{
provide: getRepositoryToken(SurveyGroup),
useValue: mockSurveyGroupRepository,
},
{
provide: getRepositoryToken(SurveyMeta),
useValue: mockSurveyMetaRepository,
},
],
}).compile();
service = module.get<SurveyGroupService>(SurveyGroupService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should create a survey group', async () => {
const createParams = { name: 'Test Group', ownerId: '123' };
const mockSavedGroup = { ...createParams, id: '1' };
mockSurveyGroupRepository.create.mockReturnValue(mockSavedGroup);
mockSurveyGroupRepository.save.mockResolvedValue(mockSavedGroup);
expect(await service.create(createParams)).toEqual(mockSavedGroup);
expect(mockSurveyGroupRepository.create).toHaveBeenCalledWith(
createParams,
);
expect(mockSurveyGroupRepository.save).toHaveBeenCalledWith(
mockSavedGroup,
);
});
});
describe('findAll', () => {
it('should return survey groups', async () => {
const list = [{ id: '1', name: 'Test Group', ownerId: '123' }];
const total = list.length;
mockSurveyGroupRepository.findAndCount.mockResolvedValue([list, total]);
mockSurveyGroupRepository.find.mockResolvedValue(list);
const result = await service.findAll('123', '', 0, 10);
expect(result).toEqual({ total, list, allList: list });
expect(mockSurveyGroupRepository.findAndCount).toHaveBeenCalled();
expect(mockSurveyGroupRepository.find).toHaveBeenCalled();
});
});
describe('update', () => {
it('should update a survey group', async () => {
const id = '1';
const updatedFields = { name: 'Updated Test Group' };
await service.update(id, updatedFields);
expect(mockSurveyGroupRepository.update).toHaveBeenCalledWith(id, {
...updatedFields,
updatedAt: expect.any(Date),
});
});
});
describe('remove', () => {
it('should remove a survey group', async () => {
const id = '1';
await service.remove(id);
expect(mockSurveyMetaRepository.updateMany).toHaveBeenCalledWith(
{ groupId: id },
{ $set: { groupId: null } },
);
expect(mockSurveyGroupRepository.delete).toHaveBeenCalledWith(id);
});
});
});

View File

@ -77,6 +77,7 @@ describe('SurveyMetaController', () => {
survey: { survey: {
title: reqBody.title, title: reqBody.title,
remark: reqBody.remark, remark: reqBody.remark,
groupId: null,
}, },
}); });

View File

@ -86,6 +86,7 @@ describe('SurveyMetaService', () => {
createMethod: params.createMethod, createMethod: params.createMethod,
createFrom: params.createFrom, createFrom: params.createFrom,
workspaceId: params.workspaceId, workspaceId: params.workspaceId,
groupId: null,
}); });
expect(surveyRepository.save).toHaveBeenCalledWith(newSurvey); expect(surveyRepository.save).toHaveBeenCalledWith(newSurvey);
expect(result).toEqual(newSurvey); expect(result).toEqual(newSurvey);

View File

@ -78,7 +78,7 @@ export class SurveyController {
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
} }
const { title, remark, createMethod, createFrom } = value; const { title, remark, createMethod, createFrom, groupId } = value;
let surveyType = '', let surveyType = '',
workspaceId = null; workspaceId = null;
@ -100,6 +100,7 @@ export class SurveyController {
createMethod, createMethod,
createFrom, createFrom,
workspaceId, workspaceId,
groupId,
}); });
await this.surveyConfService.createSurveyConf({ await this.surveyConfService.createSurveyConf({
surveyId: surveyMeta._id.toString(), surveyId: surveyMeta._id.toString(),

View File

@ -0,0 +1,145 @@
import {
Controller,
Get,
Post,
Delete,
Body,
Param,
UseGuards,
Request,
HttpCode,
Query,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import moment from 'moment';
import { Authentication } from 'src/guards/authentication.guard';
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
import { SurveyGroupService } from '../services/surveyGroup.service';
import { Logger } from 'src/logger';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { CreateSurveyGroupDto } from '../dto/createSurveyGroup.dto';
import { UpdateSurveyGroupDto } from '../dto/updateSurveyGroup.dto';
import { GetGroupListDto } from '../dto/getGroupList.dto';
@ApiTags('surveyGroup')
@ApiBearerAuth()
@UseGuards(Authentication)
@Controller('api/surveyGroup')
export class SurveyGroupController {
constructor(
private readonly surveyMetaService: SurveyMetaService,
private readonly SurveyGroupService: SurveyGroupService,
private readonly logger: Logger,
) {}
@Post()
@HttpCode(200)
async create(
@Body()
reqBody: CreateSurveyGroupDto,
@Request()
req,
) {
const { error, value } = CreateSurveyGroupDto.validate(reqBody);
if (error) {
this.logger.error(`createSurveyGroup_parameter error: ${error.message}`);
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const userId = req.user._id.toString();
const ret = await this.SurveyGroupService.create({
name: value.name,
ownerId: userId,
});
return {
code: 200,
data: {
id: ret._id,
},
};
}
@Get()
@HttpCode(200)
async findAll(@Request() req, @Query() queryInfo: GetGroupListDto) {
const { value, error } = GetGroupListDto.validate(queryInfo);
if (error) {
this.logger.error(`GetGroupListDto validate failed: ${error.message}`);
throw new HttpException(
`参数错误: 请联系管理员`,
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
const userId = req.user._id.toString();
const curPage = Number(value.curPage);
const pageSize = Number(value.pageSize);
const skip = (curPage - 1) * pageSize;
const { total, list, allList } = await this.SurveyGroupService.findAll(
userId,
value.name,
skip,
pageSize,
);
const groupIdList = list.map((item) => item._id.toString());
const surveyTotalList = await Promise.all(
groupIdList.map((item) => {
return this.surveyMetaService.countSurveyMetaByGroupId({
groupId: item,
});
}),
);
const surveyTotalMap = groupIdList.reduce((pre, cur, index) => {
const total = surveyTotalList[index];
pre[cur] = total;
return pre;
}, {});
const notTotal = await this.surveyMetaService.countSurveyMetaByGroupId({
groupId: null,
});
return {
code: 200,
data: {
total,
list: list.map((item) => {
const id = item._id.toString();
return {
...item,
createdAt: moment(item.createdAt).format('YYYY-MM-DD HH:mm:ss'),
surveyTotal: surveyTotalMap[id] || 0,
};
}),
allList,
notTotal,
},
};
}
@Post(':id')
@HttpCode(200)
async updateOne(
@Param('id') id: string,
@Body()
reqBody: UpdateSurveyGroupDto,
) {
const { error, value } = UpdateSurveyGroupDto.validate(reqBody);
if (error) {
this.logger.error(`createSurveyGroup_parameter error: ${error.message}`);
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const ret = await this.SurveyGroupService.update(id, value);
return {
code: 200,
ret,
};
}
@Delete(':id')
@HttpCode(200)
async remove(@Param('id') id: string) {
await this.SurveyGroupService.remove(id);
return {
code: 200,
};
}
}

View File

@ -48,6 +48,7 @@ export class SurveyMetaController {
title: Joi.string().required(), title: Joi.string().required(),
remark: Joi.string().allow(null, '').default(''), remark: Joi.string().allow(null, '').default(''),
surveyId: Joi.string().required(), surveyId: Joi.string().required(),
groupId: Joi.string().allow(null, ''),
}).validate(reqBody, { allowUnknown: true }); }).validate(reqBody, { allowUnknown: true });
if (error) { if (error) {
@ -57,6 +58,8 @@ export class SurveyMetaController {
const survey = req.surveyMeta; const survey = req.surveyMeta;
survey.title = value.title; survey.title = value.title;
survey.remark = value.remark; survey.remark = value.remark;
survey.groupId =
value.groupId && value.groupId !== '' ? value.groupId : null;
await this.surveyMetaService.editSurveyMeta({ await this.surveyMetaService.editSurveyMeta({
survey, survey,
@ -86,7 +89,7 @@ export class SurveyMetaController {
this.logger.error(error.message); this.logger.error(error.message);
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
} }
const { curPage, pageSize, workspaceId } = value; const { curPage, pageSize, workspaceId, groupId } = value;
let filter = {}, let filter = {},
order = {}; order = {};
if (value.filter) { if (value.filter) {
@ -120,6 +123,7 @@ export class SurveyMetaController {
filter, filter,
order, order,
workspaceId, workspaceId,
groupId,
surveyIdList, surveyIdList,
}); });
return { return {

View File

@ -20,6 +20,9 @@ export class CreateSurveyDto {
@ApiProperty({ description: '问卷创建在哪个空间下', required: false }) @ApiProperty({ description: '问卷创建在哪个空间下', required: false })
workspaceId?: string; workspaceId?: string;
@ApiProperty({ description: '问卷创建在哪个分组下', required: false })
groupId?: string;
static validate(data) { static validate(data) {
return Joi.object({ return Joi.object({
title: Joi.string().required(), title: Joi.string().required(),
@ -36,6 +39,7 @@ export class CreateSurveyDto {
otherwise: Joi.allow(null), otherwise: Joi.allow(null),
}), }),
workspaceId: Joi.string().allow(null, ''), workspaceId: Joi.string().allow(null, ''),
groupId: Joi.string().allow(null, ''),
}).validate(data); }).validate(data);
} }
} }

View File

@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
export class CreateSurveyGroupDto {
@ApiProperty({ description: '分组名称', required: true })
name: string;
static validate(data) {
return Joi.object({
name: Joi.string().required(),
}).validate(data);
}
}

View File

@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
export class GetGroupListDto {
@ApiProperty({ description: '当前页码', required: true })
curPage: number;
@ApiProperty({ description: '分页', required: false })
pageSize: number;
@ApiProperty({ description: '空间名称', required: false })
name?: string;
static validate(data: Partial<GetGroupListDto>): Joi.ValidationResult {
return Joi.object({
curPage: Joi.number().required(),
pageSize: Joi.number().allow(null).default(10),
name: Joi.string().allow(null, '').optional(),
}).validate(data);
}
}

View File

@ -17,6 +17,9 @@ export class GetSurveyListDto {
@ApiProperty({ description: '空间id', required: false }) @ApiProperty({ description: '空间id', required: false })
workspaceId?: string; workspaceId?: string;
@ApiProperty({ description: '分组id', required: false })
groupId?: string;
static validate(data) { static validate(data) {
return Joi.object({ return Joi.object({
curPage: Joi.number().required(), curPage: Joi.number().required(),
@ -24,6 +27,7 @@ export class GetSurveyListDto {
filter: Joi.string().allow(null), filter: Joi.string().allow(null),
order: Joi.string().allow(null), order: Joi.string().allow(null),
workspaceId: Joi.string().allow(null, ''), workspaceId: Joi.string().allow(null, ''),
groupId: Joi.string().allow(null, ''),
}).validate(data); }).validate(data);
} }
} }

View File

@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
export class UpdateSurveyGroupDto {
@ApiProperty({ description: '分组名称', required: true })
name: string;
static validate(data) {
return Joi.object({
name: Joi.string().required(),
}).validate(data);
}
}

View File

@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm';
import { SurveyGroup } from 'src/models/surveyGroup.entity';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
@Injectable()
export class SurveyGroupService {
constructor(
@InjectRepository(SurveyGroup)
private readonly SurveyGroup: MongoRepository<SurveyGroup>,
@InjectRepository(SurveyMeta)
private surveyMetaRepository: MongoRepository<SurveyMeta>,
) {}
create(params: { name: string; ownerId: string }) {
const newGroup = this.SurveyGroup.create({
...params,
});
return this.SurveyGroup.save(newGroup);
}
async findAll(userId: string, name: string, skip: number, pageSize: number) {
const [list, total] = await this.SurveyGroup.findAndCount({
skip: skip,
take: pageSize,
where: name
? { name: { $regex: name, $options: 'i' }, ownerId: userId }
: { ownerId: userId },
order: {
createdAt: -1,
},
});
const allList = await this.SurveyGroup.find({
where: { ownerId: userId },
select: ['_id', 'name'],
});
return {
total,
list,
allList,
};
}
update(id: string, updatedFields: Partial<SurveyGroup>) {
updatedFields.updatedAt = new Date();
return this.SurveyGroup.update(id, updatedFields);
}
async remove(id: string) {
const query = { groupId: id };
const update = { $set: { groupId: null } };
await this.surveyMetaRepository.updateMany(query, update);
return this.SurveyGroup.delete(id);
}
}

View File

@ -47,6 +47,7 @@ export class SurveyMetaService {
createMethod: string; createMethod: string;
createFrom: string; createFrom: string;
workspaceId?: string; workspaceId?: string;
groupId?: string;
}) { }) {
const { const {
title, title,
@ -57,6 +58,7 @@ export class SurveyMetaService {
createFrom, createFrom,
userId, userId,
workspaceId, workspaceId,
groupId,
} = params; } = params;
const surveyPath = await this.getNewSurveyPath(); const surveyPath = await this.getNewSurveyPath();
const newSurvey = this.surveyRepository.create({ const newSurvey = this.surveyRepository.create({
@ -71,6 +73,7 @@ export class SurveyMetaService {
createMethod, createMethod,
createFrom, createFrom,
workspaceId, workspaceId,
groupId: groupId && groupId !== '' ? groupId : null,
}); });
return await this.surveyRepository.save(newSurvey); return await this.surveyRepository.save(newSurvey);
@ -143,10 +146,18 @@ export class SurveyMetaService {
filter: Record<string, any>; filter: Record<string, any>;
order: Record<string, any>; order: Record<string, any>;
workspaceId?: string; workspaceId?: string;
groupId?: string;
surveyIdList?: Array<string>; surveyIdList?: Array<string>;
}): Promise<{ data: any[]; count: number }> { }): Promise<{ data: any[]; count: number }> {
const { pageNum, pageSize, userId, username, workspaceId, surveyIdList } = const {
condition; pageNum,
pageSize,
userId,
username,
workspaceId,
groupId,
surveyIdList,
} = condition;
const skip = (pageNum - 1) * pageSize; const skip = (pageNum - 1) * pageSize;
try { try {
const query: Record<string, any> = Object.assign( const query: Record<string, any> = Object.assign(
@ -160,6 +171,15 @@ export class SurveyMetaService {
if (condition.filter['curStatus.status']) { if (condition.filter['curStatus.status']) {
query['subStatus.status'] = RECORD_SUB_STATUS.DEFAULT; query['subStatus.status'] = RECORD_SUB_STATUS.DEFAULT;
} }
if (groupId && groupId !== 'all') {
query.groupId =
groupId === 'nogrouped'
? {
$exists: true,
$eq: null,
}
: groupId;
}
if (workspaceId) { if (workspaceId) {
query.workspaceId = workspaceId; query.workspaceId = workspaceId;
} else { } else {
@ -228,4 +248,21 @@ export class SurveyMetaService {
}); });
return total; return total;
} }
async countSurveyMetaByGroupId({ groupId }) {
const total = await this.surveyRepository.count({
groupId,
$or: [
{ workspaceId: { $exists: false } },
{ workspaceId: null },
{ workspaceId: '' },
],
isDeleted: {
$ne: true,
},
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
});
return total;
}
} }

View File

@ -17,11 +17,13 @@ import { SurveyUIController } from './controllers/surveyUI.controller';
import { CollaboratorController } from './controllers/collaborator.controller'; import { CollaboratorController } from './controllers/collaborator.controller';
import { DownloadTaskController } from './controllers/downloadTask.controller'; import { DownloadTaskController } from './controllers/downloadTask.controller';
import { SessionController } from './controllers/session.controller'; import { SessionController } from './controllers/session.controller';
import { SurveyGroupController } from './controllers/surveyGroup.controller';
import { SurveyConf } from 'src/models/surveyConf.entity'; import { SurveyConf } from 'src/models/surveyConf.entity';
import { SurveyHistory } from 'src/models/surveyHistory.entity'; import { SurveyHistory } from 'src/models/surveyHistory.entity';
import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { SurveyResponse } from 'src/models/surveyResponse.entity'; import { SurveyResponse } from 'src/models/surveyResponse.entity';
import { SurveyGroup } from 'src/models/surveyGroup.entity';
import { Word } from 'src/models/word.entity'; import { Word } from 'src/models/word.entity';
import { Collaborator } from 'src/models/collaborator.entity'; import { Collaborator } from 'src/models/collaborator.entity';
import { DownloadTask } from 'src/models/downloadTask.entity'; import { DownloadTask } from 'src/models/downloadTask.entity';
@ -38,6 +40,7 @@ import { CounterService } from '../surveyResponse/services/counter.service';
import { FileService } from '../file/services/file.service'; import { FileService } from '../file/services/file.service';
import { DownloadTaskService } from './services/downloadTask.service'; import { DownloadTaskService } from './services/downloadTask.service';
import { SessionService } from './services/session.service'; import { SessionService } from './services/session.service';
import { SurveyGroupService } from './services/surveyGroup.service';
import { Session } from 'src/models/session.entity'; import { Session } from 'src/models/session.entity';
@Module({ @Module({
@ -52,6 +55,7 @@ import { Session } from 'src/models/session.entity';
Counter, Counter,
DownloadTask, DownloadTask,
Session, Session,
SurveyGroup,
]), ]),
ConfigModule, ConfigModule,
SurveyResponseModule, SurveyResponseModule,
@ -68,6 +72,7 @@ import { Session } from 'src/models/session.entity';
CollaboratorController, CollaboratorController,
DownloadTaskController, DownloadTaskController,
SessionController, SessionController,
SurveyGroupController,
], ],
providers: [ providers: [
DataStatisticService, DataStatisticService,
@ -82,6 +87,7 @@ import { Session } from 'src/models/session.entity';
DownloadTaskService, DownloadTaskService,
FileService, FileService,
SessionService, SessionService,
SurveyGroupService,
], ],
}) })
export class SurveyModule {} export class SurveyModule {}

View File

@ -39,8 +39,8 @@
"showSpliter": true, "showSpliter": true,
"placeholder": "", "placeholder": "",
"isRequired": true, "isRequired": true,
"starMin": "", "min": "",
"starMax": "", "max": "",
"type": "radio-star", "type": "radio-star",
"title": "标题2" "title": "标题2"
} }

View File

@ -75,6 +75,11 @@ export const mockResponseSchema: ResponseSchema = {
is_again: true, is_again: true,
again_text: '确认要提交吗?', again_text: '确认要提交吗?',
}, },
jumpConfig: {
type: 'link',
link: '',
buttonText: '',
},
}, },
dataConf: { dataConf: {
dataList: [ dataList: [

View File

@ -78,3 +78,21 @@ export const getCollaboratorPermissions = (surveyId: string) => {
} }
}) })
} }
export const createGroup = ({ name }: any) => {
return axios.post('surveyGroup', { name })
}
export const updateGroup = ({ _id, name }: any) => {
return axios.post(`/surveyGroup/${_id}`, { name })
}
export const getGroupList = (params: any) => {
return axios.get('/surveyGroup', {
params
})
}
export const deleteGroup = (id: string) => {
return axios.delete(`/surveyGroup/${id}`)
}

View File

@ -1,13 +1,14 @@
import axios from './base' import axios from './base'
export const getSurveyList = ({ curPage, filter, order, workspaceId }) => { export const getSurveyList = ({ curPage, filter, order, workspaceId, groupId }) => {
return axios.get('/survey/getList', { return axios.get('/survey/getList', {
params: { params: {
pageSize: 10, pageSize: 10,
curPage, curPage,
filter, filter,
order, order,
workspaceId workspaceId,
groupId
} }
}) })
} }

View File

@ -3,6 +3,7 @@
<el-select-v2 <el-select-v2
v-model="value" v-model="value"
filterable filterable
class="search-name"
remote remote
:remote-method="remoteMethod" :remote-method="remoteMethod"
clearable clearable
@ -58,7 +59,8 @@ const value = ref('')
const selectOptions = ref<ListItem[]>([]) const selectOptions = ref<ListItem[]>([])
const loading = ref(false) const loading = ref(false)
const remoteMethod = async (query: string) => { const remoteMethod = async (q: string) => {
const query = q.trim()
if (query !== '') { if (query !== '') {
loading.value = true loading.value = true
const res: any = await getUserList(query) const res: any = await getUserList(query)

View File

@ -115,6 +115,9 @@ const rules = {
{ {
trigger: 'change', trigger: 'change',
validator: (rule: any, value: IMember[], callback: Function) => { validator: (rule: any, value: IMember[], callback: Function) => {
if (value.length === 0) {
callback('请添加协作者')
}
if (value.filter((item: IMember) => !item.role.length).length) { if (value.filter((item: IMember) => !item.role.length).length) {
callback('请设置协作者对应权限') callback('请设置协作者对应权限')
} }
@ -186,8 +189,18 @@ const handleMembersChange = (val: IMember[]) => {
} }
</script> </script>
<style lang="scss" rel="lang/scss" scoped> <style lang="scss" scoped>
.base-form-root { .base-form-root {
padding: 20px; padding: 20px;
:deep(.list-wrapper .el-select) {
.el-select__placeholder {
text-align: right;
}
}
}
:deep(.el-form-item.is-error .search-name .el-select__wrapper) {
box-shadow: 0 0 0 1px var(--el-border-color) inset;
} }
</style> </style>

View File

@ -125,6 +125,11 @@ const handleCheckAll = (val: CheckboxValueType) => {
:deep(.el-select__wrapper) { :deep(.el-select__wrapper) {
border: none; border: none;
box-shadow: none; box-shadow: none;
&:hover,
&:focus {
border: none;
box-shadow: none;
}
} }
:deep(.ishovering) { :deep(.ishovering) {
border: none; border: none;
@ -134,11 +139,11 @@ const handleCheckAll = (val: CheckboxValueType) => {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
.operation-select { // .operation-select {
:deep(.el-select__placeholder) { // :deep(.el-select__placeholder) {
text-align: right; // text-align: right;
} // }
} // }
} }
</style> </style>
<style lang="scss"> <style lang="scss">

View File

@ -34,6 +34,25 @@ export const spaceListConfig = {
} }
} }
export const groupListConfig = {
name: {
title: '分组名称',
key: 'name',
width: 200
},
surveyTotal: {
title: '问卷数',
key: 'surveyTotal',
width: 150,
tip: true
},
createdAt: {
title: '创建时间',
key: 'createdAt',
minWidth: 200
}
}
export const fieldConfig = { export const fieldConfig = {
type: { type: {
title: '类型', title: '类型',
@ -92,6 +111,16 @@ export const noSpaceSearchDataConfig = {
desc: '可以更换条件查询试试', desc: '可以更换条件查询试试',
img: '/imgs/icons/list-empty.webp' img: '/imgs/icons/list-empty.webp'
} }
export const noGroupDataConfig = {
title: '您还没有创建问卷分组',
desc: '赶快点击右上角立即创建问卷分组吧!',
img: '/imgs/icons/list-empty.webp'
}
export const noGroupSearchDataConfig = {
title: '没有满足该查询条件的问卷分组哦',
desc: '可以更换条件查询试试',
img: '/imgs/icons/list-empty.webp'
}
export const noSearchDataConfig = { export const noSearchDataConfig = {
title: '没有满足该查询条件的问卷', title: '没有满足该查询条件的问卷',
desc: '可以更换条件查询试试', desc: '可以更换条件查询试试',

View File

@ -26,6 +26,19 @@
/> />
<p class="form-item-tip">备注仅自己可见</p> <p class="form-item-tip">备注仅自己可见</p>
</el-form-item> </el-form-item>
<el-form-item prop="groupId" label="问卷分组" v-if="menuType === MenuType.PersonalGroup">
<el-select
v-model="form.groupId"
placeholder="未分组"
>
<el-option
v-for="item in groupAllList"
:key="item?._id"
:label="item?.name"
:value="item?._id"
/>
</el-select>
</el-form-item>
<el-form-item> <el-form-item>
<el-button class="create-btn" type="primary" @click="submit" :loading="!canSubmit"> <el-button class="create-btn" type="primary" @click="submit" :loading="!canSubmit">
开始创建 开始创建
@ -37,11 +50,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, toRefs } from 'vue' import { ref, reactive, computed, toRefs } from 'vue'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss' import 'element-plus/theme-chalk/src/message.scss'
import { createSurvey } from '@/management/api/survey' import { createSurvey } from '@/management/api/survey'
import { SURVEY_TYPE_LIST } from '../types' import { SURVEY_TYPE_LIST } from '../types'
import { MenuType, GroupState } from '@/management/utils/workSpace'
import { useWorkSpaceStore } from '@/management/stores/workSpace' import { useWorkSpaceStore } from '@/management/stores/workSpace'
interface Props { interface Props {
@ -53,6 +68,8 @@ const props = withDefaults(defineProps<Props>(), {
}) })
const workSpaceStore = useWorkSpaceStore() const workSpaceStore = useWorkSpaceStore()
const { groupAllList, menuType, groupId, workSpaceId } = storeToRefs(workSpaceStore)
const ruleForm = ref<any>(null) const ruleForm = ref<any>(null)
const state = reactive({ const state = reactive({
@ -62,7 +79,8 @@ const state = reactive({
canSubmit: true, canSubmit: true,
form: { form: {
title: '问卷调研', title: '问卷调研',
remark: '问卷调研' remark: '问卷调研',
groupId: groupId.value == GroupState.All || groupId.value == GroupState.Not ? '' : groupId.value
} }
}) })
const { rules, canSubmit, form } = toRefs(state) const { rules, canSubmit, form } = toRefs(state)
@ -92,8 +110,8 @@ const submit = () => {
surveyType: selectType, surveyType: selectType,
...state.form ...state.form
} }
if (workSpaceStore.workSpaceId) { if (workSpaceId.value) {
payload.workspaceId = workSpaceStore.workSpaceId payload.workspaceId = workSpaceId.value
} }
const res: any = await createSurvey(payload) const res: any = await createSurvey(payload)
if (res?.code === 200 && res?.data?.id) { if (res?.code === 200 && res?.data?.id) {

View File

@ -14,6 +14,7 @@
v-for="(content, contentIndex) in item.content" v-for="(content, contentIndex) in item.content"
:key="`${item.key}${contentIndex}`" :key="`${item.key}${contentIndex}`"
:form-config="content" :form-config="content"
v-show="isShowFormItem(content)"
> >
<Component <Component
:is="components[content.type]" :is="components[content.type]"
@ -82,6 +83,14 @@ const formFieldData = ref<Array<any>>([])
const init = ref<boolean>(true) const init = ref<boolean>(true)
const components = shallowRef<any>(props.customComponents || {}) const components = shallowRef<any>(props.customComponents || {})
const isShowFormItem = (content: any) => {
if (_isFunction(content.toggleShowFn)) {
return content.toggleShowFn(props.moduleConfig)
} else {
return true
}
}
const handleFormChange = (data: any, formConfig: any) => { const handleFormChange = (data: any, formConfig: any) => {
// //
if (_isFunction(formConfig?.valueSetter)) { if (_isFunction(formConfig?.valueSetter)) {

View File

@ -11,6 +11,7 @@
:list="item.questionList" :list="item.questionList"
:group="{ name: DND_GROUP, pull: 'clone', put: false }" :group="{ name: DND_GROUP, pull: 'clone', put: false }"
:clone="createNewQuestion" :clone="createNewQuestion"
@end="onDragEnd"
item-key="path" item-key="path"
> >
<template #item="{ element }"> <template #item="{ element }">
@ -53,7 +54,7 @@ import { ref } from 'vue'
const editStore = useEditStore() const editStore = useEditStore()
const { newQuestionIndex } = storeToRefs(editStore) const { newQuestionIndex } = storeToRefs(editStore)
const { addQuestion, setCurrentEditOne, createNewQuestion } = editStore const { addQuestion, hasSetCurrentEditOne, createNewQuestion } = editStore
const activeNames = ref([0, 1]) const activeNames = ref([0, 1])
const previewImg = ref('') const previewImg = ref('')
@ -67,7 +68,11 @@ questionLoader.init({
const onQuestionType = ({ type }) => { const onQuestionType = ({ type }) => {
const newQuestion = createNewQuestion({ type }) const newQuestion = createNewQuestion({ type })
addQuestion({ question: newQuestion, index: newQuestionIndex.value }) addQuestion({ question: newQuestion, index: newQuestionIndex.value })
setCurrentEditOne(newQuestionIndex.value) hasSetCurrentEditOne(newQuestionIndex.value)
}
const onDragEnd = (event) => {
hasSetCurrentEditOne(event.newIndex)
} }
const showPreview = ({ snapshot }, id) => { const showPreview = ({ snapshot }, id) => {

View File

@ -5,7 +5,7 @@
</div> </div>
<SetterField <SetterField
class="question-config-form" class="question-config-form"
label-position="top" label-position="left"
:form-config-list="formFields" :form-config-list="formFields"
:module-config="moduleConfig" :module-config="moduleConfig"
@form-change="handleFormChange" @form-change="handleFormChange"
@ -71,4 +71,25 @@ const handleFormChange = ({ key, value }: any) => {
.question-config-form { .question-config-form {
padding: 30px 20px 50px 20px; padding: 30px 20px 50px 20px;
} }
:deep(.group-wrap) {
margin-bottom: 0;
&:not(:last-child) {
margin-bottom: 32px;
}
.group-title {
margin-bottom: 12px;
}
.el-form-item {
margin-bottom: 16px;
.el-radio {
height: initial;
line-height: initial;
margin-bottom: initial;
}
}
}
</style> </style>

View File

@ -5,7 +5,9 @@
<img src="/imgs/icons/success.webp" class="success-img" /> <img src="/imgs/icons/success.webp" class="success-img" />
<div class="title-msg" v-safe-html="successText"></div> <div class="title-msg" v-safe-html="successText"></div>
</div> </div>
<div class="bottom-btn"></div> <div v-if="jumpConfig.buttonText && jumpConfig.type === 'button'" class="jump-btn">
{{ jumpConfig.buttonText }}
</div>
</div> </div>
</div> </div>
</template> </template>
@ -17,6 +19,7 @@ interface Props {
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const successText = computed(() => props.moduleConfig?.msgContent?.msg_200 || '') const successText = computed(() => props.moduleConfig?.msgContent?.msg_200 || '')
const jumpConfig = computed(() => props.moduleConfig?.jumpConfig || {})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/*成功页面跳转全屏展示浮层*/ /*成功页面跳转全屏展示浮层*/
@ -50,7 +53,18 @@ const successText = computed(() => props.moduleConfig?.msgContent?.msg_200 || ''
font-size: 0.36rem; font-size: 0.36rem;
} }
} }
.bottom-btn { .jump-btn {
height: 300px; background: var(--primary-color);
width: 90%;
border-radius: 0.08rem;
padding: 0.25rem 0;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.25rem;
font-weight: 500;
margin: 0 auto;
border: none;
} }
</style> </style>

View File

@ -16,7 +16,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { watch, ref } from 'vue' import { watch, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useEditStore } from '@/management/stores/edit'
const editStore = useEditStore()
const { setCurrentEditOne } = editStore
const routes = [ const routes = [
{ {
text: '内容设置', text: '内容设置',
@ -38,6 +40,7 @@ watch(
activeRouter, activeRouter,
(val: any) => { (val: any) => {
// query // query
setCurrentEditOne(null)
const query = route.query const query = route.query
router.push({ name: val, query }) router.push({ name: val, query })
}, },

View File

@ -103,7 +103,7 @@ export default {
type: 'WhiteList', type: 'WhiteList',
custom: true, // 自定义导入高级组件 custom: true, // 自定义导入高级组件
relyFunc: (data) => { relyFunc: (data) => {
return data.whitelistType === 'CUSTOM' return data.whitelistType == 'CUSTOM'
} }
}, },
team_list: { team_list: {
@ -112,7 +112,7 @@ export default {
type: 'TeamMemberList', type: 'TeamMemberList',
custom: true, // 自定义导入高级组件 custom: true, // 自定义导入高级组件
relyFunc: (data) => { relyFunc: (data) => {
return data.whitelistType === 'MEMBER' return data.whitelistType == 'MEMBER'
} }
}, },
} }

View File

@ -1,7 +1,7 @@
export default { export default {
Success: [ Success: [
{ {
label: '提示文案', title: '提示文案',
type: 'RichText', type: 'RichText',
key: 'msgContent.msg_200', key: 'msgContent.msg_200',
placeholder: '提交成功', placeholder: '提交成功',
@ -9,6 +9,46 @@ export default {
labelStyle: { labelStyle: {
'font-weight': 'bold' 'font-weight': 'bold'
} }
},
{
title: '交卷跳转',
type: 'Customed',
key: 'jumpConfig',
content: [
{
key: 'jumpConfig.type',
type: 'RadioGroup',
value: 'link',
options: [
{
label: '跳转网页',
value: 'link'
},
{
label: '跳转按钮',
value: 'button'
},
],
},
{
key: 'jumpConfig.buttonText',
label: '按钮文案',
type: 'InputSetter',
placeholder: '请输入按钮文案',
value: '',
toggleShowFn: (data) => {
return data?.jumpConfig?.type === 'button'
},
},
{
key: 'jumpConfig.link',
label: '跳转链接',
type: 'InputSetter',
placeholder: '请输入网址',
value: '',
},
]
} }
], ],
OverTime: [ OverTime: [

View File

@ -98,6 +98,8 @@
:type="modifyType" :type="modifyType"
:visible="showModify" :visible="showModify"
:question-info="questionInfo" :question-info="questionInfo"
:group-all-list="groupAllList"
:menu-type="menuType"
@on-close-codify="onCloseModify" @on-close-codify="onCloseModify"
/> />
<CooperModify :modifyId="cooperId" :visible="cooperModify" @on-close-codify="onCooperClose" /> <CooperModify :modifyId="cooperId" :visible="cooperModify" @on-close-codify="onCooperClose" />
@ -142,7 +144,7 @@ import {
const surveyListStore = useSurveyListStore() const surveyListStore = useSurveyListStore()
const workSpaceStore = useWorkSpaceStore() const workSpaceStore = useWorkSpaceStore()
const { workSpaceId } = storeToRefs(workSpaceStore) const { workSpaceId, groupAllList, menuType } = storeToRefs(workSpaceStore)
const router = useRouter() const router = useRouter()
const props = defineProps({ const props = defineProps({
loading: { loading: {
@ -363,7 +365,6 @@ const onDelete = async (row) => {
type: 'warning' type: 'warning'
}) })
} catch (error) { } catch (error) {
console.log('取消删除')
return return
} }
@ -371,6 +372,8 @@ const onDelete = async (row) => {
if (res.code === CODE_MAP.SUCCESS) { if (res.code === CODE_MAP.SUCCESS) {
ElMessage.success('删除成功') ElMessage.success('删除成功')
onRefresh() onRefresh()
workSpaceStore.getGroupList()
workSpaceStore.getSpaceList()
} else { } else {
ElMessage.error(res.errmsg || '删除失败') ElMessage.error(res.errmsg || '删除失败')
} }
@ -409,6 +412,8 @@ const onCloseModify = (type) => {
questionInfo.value = {} questionInfo.value = {}
if (type === 'update') { if (type === 'update') {
onRefresh() onRefresh()
workSpaceStore.getGroupList()
workSpaceStore.getSpaceList()
} }
} }
const onRowClick = (row) => { const onRowClick = (row) => {

View File

@ -0,0 +1,245 @@
<template>
<div class="search">
<TextSearch placeholder="请输入分组名称" :value="searchVal" @search="onSearchText" />
</div>
<div class="list-wrap" v-if="props.total">
<el-table
v-if="props.total"
ref="multipleListTable"
class="list-table"
:data="data"
empty-text="暂无数据"
row-key="_id"
header-row-class-name="tableview-header"
row-class-name="tableview-row"
cell-class-name="tableview-cell"
v-loading="loading"
:height="550"
style="width: 100%"
>
<el-table-column column-key="space" width="20" />
<el-table-column
v-for="field in fieldList"
:key="(field as any)?.key"
:label="(field as any).title"
:column-key="(field as any).key"
:width="(field as any).width"
:min-width="(field as any).width || (field as any).minWidth"
class-name="link"
>
<template #default="scope">
<template v-if="(field as any).comp">
<component :is="(field as any).comp" type="table" :value="scope.row" />
</template>
<template v-else>
<span class="cell-span">{{ scope.row[(field as any).key] }}</span>
</template>
</template>
</el-table-column>
<el-table-column
label="操作"
:width="200"
label-class-name="operation"
class-name="table-options"
>
<template #default="scope">
<div class="space-tool-bar">
<ToolBar
:data="scope.row"
:tool-width="50"
:tools="tools"
@click="handleClick"
/>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div v-else>
<EmptyIndex :data="!searchVal ? noGroupDataConfig : noGroupSearchDataConfig" />
</div>
<div class="list-pagination">
<el-pagination
v-if="props.total"
v-model:current-page="curPage"
background
@current-change="handleCurrentChange"
layout="prev, pager, next"
:total="props.total"
>
</el-pagination>
</div>
<GroupModify
v-if="showGroupModify"
type="edit"
:visible="showGroupModify"
@on-close-codify="onCloseModify"
/>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { ElMessageBox } from 'element-plus'
import 'element-plus/theme-chalk/src/message-box.scss'
import { get, map } from 'lodash-es'
import {
noGroupDataConfig,
noGroupSearchDataConfig,
groupListConfig
} from '@/management/config/listConfig'
import { MenuType } from '@/management/utils/workSpace'
import GroupModify from './GroupModify.vue'
import TextSearch from '@/management/pages/list/components/TextSearch.vue'
import EmptyIndex from '@/management/components/EmptyIndex.vue'
import ToolBar from './ToolBar.vue'
import { useWorkSpaceStore } from '@/management/stores/workSpace'
import { useSurveyListStore } from '@/management/stores/surveyList'
const workSpaceStore = useWorkSpaceStore()
const surveyListStore = useSurveyListStore()
const showGroupModify = ref(false)
const props = defineProps({
loading: {
type: Boolean,
default: false
},
data: {
type: Array,
default: () => []
},
total: {
type: Number,
default: 0
}
})
const emit = defineEmits(['refresh'])
const fields = ['name', 'surveyTotal', 'createdAt']
const fieldList = computed(() => {
return map(fields, (f) => {
return get(groupListConfig, f, null)
})
})
const tools = ref([{
key: 'open',
label: '进入'
}, {
key: 'modify',
label: '管理'
}, {
key: 'delete',
label: '删除'
}])
const data = computed(() => {
return props.data
})
let searchVal = ref('')
let curPage = ref(1)
const emitRefresh = (page: number, name: string) => {
curPage.value = page
emit('refresh', {
curPage: page,
name
})
}
const handleCurrentChange = async (val: number) => {
emitRefresh(val, searchVal.value)
}
const onSearchText = async (value: string) => {
searchVal.value = value
emitRefresh(1, value)
}
const handleModify = (id: string) => {
workSpaceStore.getGroupDetail(id)
showGroupModify.value = true
}
const handleDelete = (id: string) => {
ElMessageBox.confirm(
'删除分组后,属于该分组的问卷将会自动更换到“未分组”下,是否确认本次删除?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(async () => {
await workSpaceStore.deleteGroup(id)
await workSpaceStore.getGroupList()
})
.catch(() => {})
}
const handleClick = (key: string, data: any) => {
if (key === 'modify') {
handleModify(data._id)
} else if (key === 'delete') {
handleDelete(data._id)
} else if(key === 'open') {
workSpaceStore.changeMenuType(MenuType.PersonalGroup)
workSpaceStore.changeGroup(data._id)
surveyListStore.getSurveyList({
pageSize: 10,
curPage: 1
})
}
}
const onCloseModify = () => {
showGroupModify.value = false
workSpaceStore.getGroupList()
}
defineExpose({ onCloseModify })
</script>
<style lang="scss" scoped>
.search {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
}
.list-pagination {
margin-top: 20px;
:deep(.el-pagination) {
display: flex;
justify-content: flex-end;
}
}
.list-wrap {
padding: 20px;
background: #fff;
.search {
display: flex;
}
.list-table {
:deep(.el-table__header) {
.tableview-header .el-table__cell {
.cell {
height: 24px;
color: #4a4c5b;
font-size: 14px;
}
}
}
:deep(.tableview-row) {
.tableview-cell {
height: 42px;
&.link {
cursor: pointer;
}
.cell .cell-span {
font-size: 14px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,119 @@
<template>
<el-dialog
class="base-dialog-root"
:model-value="visible"
width="40%"
:title="formTitle"
@close="onClose"
>
<el-form
class="base-form-root"
ref="ruleForm"
:model="formModel"
:rules="rules"
label-position="top"
size="large"
@submit.prevent
>
<el-form-item label="分组名称" prop="name">
<el-input v-model="formModel.name" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="onClose">取消</el-button>
<el-button type="primary" class="save-btn" @click="onConfirm">
确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref, shallowRef, onMounted } from 'vue'
import { pick as _pick } from 'lodash-es'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import { QOP_MAP } from '@/management/utils/constant'
import { type IGroup } from '@/management/utils/workSpace'
import { useWorkSpaceStore } from '@/management/stores/workSpace'
const workSpaceStore = useWorkSpaceStore()
const emit = defineEmits(['on-close-codify'])
const props = defineProps({
type: String,
width: String,
visible: Boolean
})
const ruleForm = shallowRef<any>(null)
const formTitle = computed(() => {
return props.type === QOP_MAP.ADD ? '创建分组' : '管理分组'
})
const formModel = ref<Required<IGroup>>({
_id: '',
name: ''
})
const rules = {
name: [{ required: true, message: '请输入分组名称', trigger: 'blur' }]
}
const groupDetail = computed(() => {
return workSpaceStore.groupDetail
})
onMounted(() => {
if (props.type === QOP_MAP.EDIT) {
formModel.value = _pick(groupDetail.value as any, ['_id', 'name'])
}
})
const onClose = () => {
formModel.value = {
_id: '',
name: ''
}
//
workSpaceStore.setGroupDetail(null)
emit('on-close-codify')
}
const onConfirm = async () => {
ruleForm.value?.validate(async (valid: boolean) => {
if (valid) {
if (props.type === QOP_MAP.ADD) {
try {
await handleAdd()
emit('on-close-codify')
} catch (err) {
ElMessage.error('createGroup status err' + err)
}
} else {
try {
await handleUpdate()
emit('on-close-codify')
} catch (err) {
ElMessage.error('createGroup status err' + err)
}
}
} else {
return false
}
})
}
const handleUpdate = async () => {
await workSpaceStore.updateGroup(formModel.value)
}
const handleAdd = async () => {
await workSpaceStore.addGroup({ name: formModel.value.name })
}
</script>
<style lang="scss" rel="lang/scss" scoped>
.base-form-root {
padding: 20px;
}
</style>

View File

@ -21,6 +21,19 @@
<el-form-item label="备注"> <el-form-item label="备注">
<el-input v-model="current.remark" /> <el-input v-model="current.remark" />
</el-form-item> </el-form-item>
<el-form-item prop="groupId" label="问卷分组" v-if="menuType === MenuType.PersonalGroup">
<el-select
v-model="current.groupId"
placeholder="未分组"
>
<el-option
v-for="item in groupAllList"
:key="item._id"
:label="item.name"
:value="item._id"
/>
</el-select>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@ -35,7 +48,6 @@
<script> <script>
import { pick as _pick } from 'lodash-es' import { pick as _pick } from 'lodash-es'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss' import 'element-plus/theme-chalk/src/message.scss'
@ -43,17 +55,22 @@ import { CODE_MAP } from '@/management/api/base'
import { updateSurvey, createSurvey } from '@/management/api/survey' import { updateSurvey, createSurvey } from '@/management/api/survey'
import { QOP_MAP } from '@/management/utils/constant' import { QOP_MAP } from '@/management/utils/constant'
import { MenuType } from '@/management/utils/workSpace'
export default { export default {
name: 'ModifyDialog', name: 'ModifyDialog',
props: { props: {
type: String, type: String,
questionInfo: Object, questionInfo: Object,
width: String, width: String,
visible: Boolean visible: Boolean,
groupAllList: Array,
menuType: String,
}, },
data() { data() {
return { return {
QOP_MAP, QOP_MAP,
MenuType,
loadingInstance: null, loadingInstance: null,
rules: { rules: {
title: [{ required: true, message: '请输入问卷标题', trigger: 'blur' }] title: [{ required: true, message: '请输入问卷标题', trigger: 'blur' }]
@ -72,7 +89,8 @@ export default {
methods: { methods: {
getCurrent(val) { getCurrent(val) {
return { return {
..._pick(val, ['title', 'remark']) ..._pick(val, ['title', 'remark']),
groupId: val.groupId === null ? '' : val.groupId
} }
}, },
onClose() { onClose() {

View File

@ -1,14 +1,16 @@
<template> <template>
<el-menu <el-menu
:default-active="SpaceType.Personal" :default-active="active"
class="el-menu-vertical" class="el-menu-vertical"
ref="menuRef" ref="menuRef"
@select="handleSelect" @select="handleMenu"
@open="handleMenu"
@close="handleMenu"
> >
<template v-for="(menu, index) in menus" :key="menu.id"> <template v-for="(menu, index) in props.menus" :key="menu.id">
<el-menu-item <el-menu-item
:class="[index === 0 ? 'bottom' : '', index > 2 ? 'sub-item' : 'main-item']" :class="[index === 0 ? 'bottom' : '', index > 2 ? 'sub-item' : 'main-item', active == menu.id ? 'check-item' : '' ]"
:index="menu.id" :index="menu.id.toString()"
v-if="!menu.children?.length" v-if="!menu.children?.length"
> >
<template #title> <template #title>
@ -18,46 +20,65 @@
</div> </div>
</template> </template>
</el-menu-item> </el-menu-item>
<el-menu-item-group v-else> <el-sub-menu v-else :index="menu.id.toString()" :class="[ active == menu.id ? 'check-item' : '' ]">
<template #title> <template #title>
<el-menu-item :index="menu.id" class="sub-title main-item"> <div class="title-content sub-title main-item">
<div class="title-content">
<i :class="['iconfont', menu.icon]"></i> <i :class="['iconfont', menu.icon]"></i>
<span>{{ menu.name }}</span> <span>{{ menu.name }}</span>
</div> </div>
</el-menu-item>
</template> </template>
<el-menu-item v-for="item in menu.children" :key="item.id" :index="item.id"> <el-menu-item v-for="item in menu.children" :key="item.id" :index="item.id.toString()" :class="[ active == item.id ? 'check-item' : '' ]">
<p> <div class="title-box">
{{ item.name }} <p class="title-text">{{ item.name }}</p>
</p> <p class="title-total">{{ item.total }}</p>
</div>
</el-menu-item> </el-menu-item>
</el-menu-item-group> </el-sub-menu>
</template> </template>
</el-menu> </el-menu>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, computed } from 'vue'
import { type MenuItem } from '@/management/utils/workSpace' import { type MenuItem } from '@/management/utils/workSpace'
import { SpaceType } from '@/management/utils/workSpace' import { MenuType } from '@/management/utils/workSpace'
const menuRef = ref() const menuRef = ref()
const props = withDefaults(
withDefaults(
defineProps<{ defineProps<{
menus: Array<MenuItem> menus: Array<MenuItem>,
activeValue: string
}>(), }>(),
{ {
menus: () => [] menus: () => [],
activeValue: MenuType.PersonalGroup
} }
) )
const active = computed({
get: () => {
return props.activeValue
},
set: () => {}
})
const emit = defineEmits(['select']) const emit = defineEmits(['select'])
const handleSelect = (id: string) => { const handleMenu = (id: string) => {
active.value = id
emit('select', id) emit('select', id)
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.el-sub-menu {
:deep(.el-sub-menu__icon-arrow) {
transform: rotate(-90deg) !important;
}
&.is-opened {
> :deep(.el-sub-menu__title .el-sub-menu__icon-arrow) {
transform: rotate(0deg) !important;
}
}
}
.el-menu-vertical { .el-menu-vertical {
border: none; border: none;
width: 200px; width: 200px;
@ -94,10 +115,6 @@ const handleSelect = (id: string) => {
&.sub-item { &.sub-item {
margin: 0; margin: 0;
} }
&.is-active {
// background-color: #F2F4F7;
background: #fef6e6 100% !important;
}
&:hover { &:hover {
background-color: #f2f4f7; background-color: #f2f4f7;
} }
@ -106,6 +123,27 @@ const handleSelect = (id: string) => {
align-items: center; align-items: center;
font-weight: 400; font-weight: 400;
} }
.title-box {
width: 100%;
display: flex;
justify-content: space-between;
}
.title-text {
width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title-total {
font-size: 14px;
color: #92949D;
text-align: right;
font-weight: 400;
}
} }
:deep(.el-menu-item-group) { :deep(.el-menu-item-group) {
> ul { > ul {
@ -128,4 +166,7 @@ const handleSelect = (id: string) => {
margin-right: 10px; margin-right: 10px;
color: #faa600 !important; color: #faa600 !important;
} }
.check-item {
background: #fef6e6 100% !important
}
</style> </style>

View File

@ -2,7 +2,8 @@
<div class="search"> <div class="search">
<TextSearch placeholder="请输入空间名称" :value="searchVal" @search="onSearchText" /> <TextSearch placeholder="请输入空间名称" :value="searchVal" @search="onSearchText" />
</div> </div>
<div class="list-wrap" v-if="props.total > 0"> <template v-if="total > 0">
<div class="list-wrap">
<el-table <el-table
ref="multipleListTable" ref="multipleListTable"
class="list-table" class="list-table"
@ -54,9 +55,6 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div>
<div v-else>
<EmptyIndex :data="!searchVal ? noSpaceDataConfig : noSpaceSearchDataConfig" />
</div>
<div class="list-pagination"> <div class="list-pagination">
<el-pagination <el-pagination
v-model:current-page="curPage" v-model:current-page="curPage"
@ -67,6 +65,12 @@
> >
</el-pagination> </el-pagination>
</div> </div>
</template>
<div v-else>
<EmptyIndex :data="!searchVal ? noSpaceDataConfig : noSpaceSearchDataConfig" />
</div>
<SpaceModify <SpaceModify
v-if="showSpaceModify" v-if="showSpaceModify"
:type="modifyType" :type="modifyType"

View File

@ -55,7 +55,7 @@ import { useWorkSpaceStore } from '@/management/stores/workSpace'
import MemberSelect from '@/management/components/CooperModify/MemberSelect.vue' import MemberSelect from '@/management/components/CooperModify/MemberSelect.vue'
const workSpaceStore = useWorkSpaceStore() const workSpaceStore = useWorkSpaceStore()
const emit = defineEmits(['on-close-codify', 'onFocus', 'change', 'blur']) const emit = defineEmits(['on-close-codify', 'onFocus', 'change', 'blur', 'updateData'])
const props = defineProps({ const props = defineProps({
type: String, type: String,
width: String, width: String,
@ -128,6 +128,7 @@ const onConfirm = async () => {
} else { } else {
try { try {
await handleUpdate() await handleUpdate()
emit('updateData', formModel.value)
emit('on-close-codify', 'update') emit('on-close-codify', 'update')
} catch (err) { } catch (err) {
ElMessage.error('createSpace status err' + err) ElMessage.error('createSpace status err' + err)

View File

@ -2,23 +2,32 @@
<div class="question-list-root"> <div class="question-list-root">
<TopNav></TopNav> <TopNav></TopNav>
<div class="content-wrap"> <div class="content-wrap">
<SliderBar :menus="spaceMenus" @select="handleSpaceSelect" /> <SliderBar :menus="spaceMenus" :activeValue="activeValue" @select="handleSpaceSelect" />
<div class="list-content"> <div class="list-content">
<div class="top"> <div class="top">
<h2> <h2>
{{ spaceType === SpaceType.Group ? '团队空间' : currentTeamSpace?.name || '问卷列表' }} {{ tableTitle }}
</h2> </h2>
<div class="operation"> <div class="operation">
<el-button <el-button
class="btn create-btn" class="btn create-btn"
type="default" type="default"
@click="onSpaceCreate" @click="onSpaceCreate"
v-if="spaceType == SpaceType.Group" v-if="menuType === MenuType.SpaceGroup && !workSpaceId"
> >
<i class="iconfont icon-chuangjian"></i> <i class="iconfont icon-chuangjian"></i>
<span>创建团队空间</span> <span>创建团队空间</span>
</el-button> </el-button>
<el-button type="default" class="btn" @click="onSetGroup" v-if="workSpaceId"> <el-button
class="btn create-btn"
type="default"
@click="onGroupCreate"
v-if="menuType === MenuType.PersonalGroup && !groupId"
>
<i class="iconfont icon-chuangjian"></i>
<span>创建分组</span>
</el-button>
<el-button type="default" class="btn" @click="onSetGroup" v-if="workSpaceId && menuType === MenuType.SpaceGroup">
<i class="iconfont icon-shujuliebiao"></i> <i class="iconfont icon-shujuliebiao"></i>
<span>团队管理</span> <span>团队管理</span>
</el-button> </el-button>
@ -26,7 +35,7 @@
class="btn create-btn" class="btn create-btn"
type="default" type="default"
@click="onCreate" @click="onCreate"
v-if="spaceType !== SpaceType.Group" v-if="workSpaceId || groupId"
> >
<i class="iconfont icon-chuangjian"></i> <i class="iconfont icon-chuangjian"></i>
<span>创建问卷</span> <span>创建问卷</span>
@ -38,7 +47,7 @@
:data="surveyList" :data="surveyList"
:total="surveyTotal" :total="surveyTotal"
@refresh="fetchSurveyList" @refresh="fetchSurveyList"
v-if="spaceType !== SpaceType.Group" v-if="workSpaceId || groupId"
></BaseList> ></BaseList>
<SpaceList <SpaceList
ref="spaceListRef" ref="spaceListRef"
@ -46,15 +55,30 @@
:loading="spaceLoading" :loading="spaceLoading"
:data="workSpaceList" :data="workSpaceList"
:total="workSpaceListTotal" :total="workSpaceListTotal"
v-if="spaceType === SpaceType.Group" v-if="menuType === MenuType.SpaceGroup && !workSpaceId"
></SpaceList> ></SpaceList>
<GroupList
ref="groupListRef"
@refresh="fetchGroupList"
:loading="groupLoading"
:data="groupList"
:total="groupListTotal"
v-if="menuType === MenuType.PersonalGroup && !groupId"
></GroupList>
</div> </div>
</div> </div>
<SpaceModify <SpaceModify
v-if="showSpaceModify" v-if="showSpaceModify"
:type="modifyType" :type="modifyType"
:visible="showSpaceModify" :visible="showSpaceModify"
@on-close-codify="onCloseModify" @on-close-codify="onCloseSpaceModify"
@update-data="onCloseModifyInTeamWork"
/>
<GroupModify
v-if="showGroupModify"
type="add"
:visible="showGroupModify"
@on-close-codify="onCloseGroupModify"
/> />
</div> </div>
</template> </template>
@ -65,25 +89,54 @@ import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import BaseList from './components/BaseList.vue' import BaseList from './components/BaseList.vue'
import SpaceList from './components/SpaceList.vue' import SpaceList from './components/SpaceList.vue'
import GroupList from './components/GroupList.vue'
import SliderBar from './components/SliderBar.vue' import SliderBar from './components/SliderBar.vue'
import SpaceModify from './components/SpaceModify.vue' import SpaceModify from './components/SpaceModify.vue'
import GroupModify from './components/GroupModify.vue'
import TopNav from '@/management/components/TopNav.vue' import TopNav from '@/management/components/TopNav.vue'
import { SpaceType } from '@/management/utils/workSpace' import { MenuType } from '@/management/utils/workSpace'
import { useWorkSpaceStore } from '@/management/stores/workSpace' import { useWorkSpaceStore } from '@/management/stores/workSpace'
import { useSurveyListStore } from '@/management/stores/surveyList' import { useSurveyListStore } from '@/management/stores/surveyList'
import { type IWorkspace } from '@/management/utils/workSpace'
const workSpaceStore = useWorkSpaceStore() const workSpaceStore = useWorkSpaceStore()
const surveyListStore = useSurveyListStore() const surveyListStore = useSurveyListStore()
const { surveyList, surveyTotal } = storeToRefs(surveyListStore) const { surveyList, surveyTotal } = storeToRefs(surveyListStore)
const { spaceMenus, workSpaceId, spaceType, workSpaceList, workSpaceListTotal } = const { spaceMenus, workSpaceId, groupId, menuType, workSpaceList, workSpaceListTotal, groupList, groupListTotal } =
storeToRefs(workSpaceStore) storeToRefs(workSpaceStore)
const router = useRouter() const router = useRouter()
const tableTitle = computed(() => {
if(menuType.value === MenuType.PersonalGroup && !groupId.value) {
return '我的空间'
} else if (menuType.value === MenuType.SpaceGroup && !workSpaceId.value) {
return '团队空间'
} else {
return currentTeamSpace.value?.name || '问卷列表';
}
})
const activeValue = computed(() => {
if(workSpaceId.value !== '') {
return workSpaceId.value
} else if(groupId.value !== '') {
return groupId.value
} else if(menuType.value === MenuType.PersonalGroup) {
return MenuType.PersonalGroup
} else if(menuType.value === MenuType.SpaceGroup) {
return MenuType.SpaceGroup
} else {
return ''
}
})
const loading = ref(false) const loading = ref(false)
const spaceListRef = ref<any>(null) const spaceListRef = ref<any>(null)
const spaceLoading = ref(false) const spaceLoading = ref(false)
const groupLoading = ref(false)
const fetchSpaceList = async (params?: any) => { const fetchSpaceList = async (params?: any) => {
spaceLoading.value = true spaceLoading.value = true
@ -92,24 +145,39 @@ const fetchSpaceList = async (params?: any) => {
spaceLoading.value = false spaceLoading.value = false
} }
const handleSpaceSelect = (id: SpaceType | string) => { const fetchGroupList = async (params?: any) => {
if (id === spaceType.value || id === workSpaceId.value) { groupLoading.value = true
workSpaceStore.changeWorkSpace('')
workSpaceStore.getGroupList(params)
groupLoading.value = false
}
const handleSpaceSelect = (id: MenuType | string) => {
if (groupId.value === id || workSpaceId.value === id) {
return void 0 return void 0
} }
let parentMenu = undefined
switch (id) { switch (id) {
case SpaceType.Personal: case MenuType.PersonalGroup:
workSpaceStore.changeSpaceType(SpaceType.Personal) workSpaceStore.changeMenuType(MenuType.PersonalGroup)
workSpaceStore.changeWorkSpace('') workSpaceStore.changeWorkSpace('')
fetchGroupList()
break break
case SpaceType.Group: case MenuType.SpaceGroup:
workSpaceStore.changeSpaceType(SpaceType.Group) workSpaceStore.changeMenuType(MenuType.SpaceGroup)
workSpaceStore.changeWorkSpace('') workSpaceStore.changeWorkSpace('')
fetchSpaceList() fetchSpaceList()
break break
default: default:
workSpaceStore.changeSpaceType(SpaceType.Teamwork) parentMenu = spaceMenus.value.find((parent: any) => parent.children.find((children: any) => children.id.toString() === id))
if(parentMenu != undefined) {
workSpaceStore.changeMenuType(parentMenu.id)
if(parentMenu.id === MenuType.PersonalGroup) {
workSpaceStore.changeGroup(id)
} else if (parentMenu.id === MenuType.SpaceGroup) {
workSpaceStore.changeWorkSpace(id) workSpaceStore.changeWorkSpace(id)
}
}
break break
} }
fetchSurveyList() fetchSurveyList()
@ -131,6 +199,7 @@ const fetchSurveyList = async (params?: any) => {
} }
onMounted(() => { onMounted(() => {
fetchGroupList()
fetchSpaceList() fetchSpaceList()
fetchSurveyList() fetchSurveyList()
}) })
@ -149,7 +218,24 @@ const onSetGroup = async () => {
showSpaceModify.value = true showSpaceModify.value = true
} }
const onCloseModify = (type: string) => { const onCloseModifyInTeamWork = (data: IWorkspace) => {
if (activeValue.value === MenuType.SpaceGroup) {
const currentData = workSpaceList.value.find((item) => item._id === data._id)
if (currentData) {
currentData.name = data.name
currentData.memberTotal = data.members.length
currentData.description = data.description
}
const currentMenus: any = spaceMenus.value?.[1]?.children?.find(
(item: { id: string; name: string }) => item.id === data._id
)
if (currentMenus) {
currentMenus.name = data.name
}
}
}
const onCloseSpaceModify = (type: string) => {
showSpaceModify.value = false showSpaceModify.value = false
if (type === 'update' && spaceListRef.value) { if (type === 'update' && spaceListRef.value) {
fetchSpaceList() fetchSpaceList()
@ -160,6 +246,20 @@ const onSpaceCreate = () => {
modifyType.value = 'add' modifyType.value = 'add'
showSpaceModify.value = true showSpaceModify.value = true
} }
//
const showGroupModify = ref<boolean>(false)
const onCloseGroupModify = () => {
showGroupModify.value = false
fetchGroupList()
}
const onGroupCreate = () => {
showGroupModify.value = true
}
const onCreate = () => { const onCreate = () => {
router.push('/create') router.push('/create')
} }

View File

@ -166,10 +166,16 @@ export const useEditStore = defineStore('edit', () => {
if (!questCount) { if (!questCount) {
return startIndex return startIndex
} }
return endIndex return endIndex - 1
} }
}) })
const hasSetCurrentEditOne = (value: number) => {
if(!currentEditOne.value) {
setCurrentEditOne(value)
}
}
// 题目操作:增删改 // 题目操作:增删改
const { copyQuestion, addQuestion, deleteQuestion, moveQuestion } = useQuestionData({ const { copyQuestion, addQuestion, deleteQuestion, moveQuestion } = useQuestionData({
questionDataList, questionDataList,
@ -194,6 +200,7 @@ export const useEditStore = defineStore('edit', () => {
currentEditMeta, currentEditMeta,
newQuestionIndex, newQuestionIndex,
setCurrentEditOne, setCurrentEditOne,
hasSetCurrentEditOne,
changeCurrentEditStatus, changeCurrentEditStatus,
pageEditOne, pageEditOne,

View File

@ -6,7 +6,7 @@ import 'element-plus/theme-chalk/src/message.scss'
import { CODE_MAP } from '@/management/api/base' import { CODE_MAP } from '@/management/api/base'
import { getSurveyList as getSurveyListReq } from '@/management/api/survey' import { getSurveyList as getSurveyListReq } from '@/management/api/survey'
import { GroupState } from '@/management/utils/workSpace'
import { useWorkSpaceStore } from './workSpace' import { useWorkSpaceStore } from './workSpace'
import { import {
@ -150,7 +150,8 @@ export const useSurveyListStore = defineStore('surveyList', () => {
pageSize: payload?.pageSize || 10, // 默认一页10条 pageSize: payload?.pageSize || 10, // 默认一页10条
filter: filterString, filter: filterString,
order: orderString, order: orderString,
workspaceId: workSpaceStore.workSpaceId workspaceId: workSpaceStore.workSpaceId,
groupId: workSpaceStore.groupId === GroupState.All ? '' : workSpaceStore.groupId
} }
const res: any = await getSurveyListReq(params) const res: any = await getSurveyListReq(params)

View File

@ -10,11 +10,16 @@ import {
updateSpace as updateSpaceReq, updateSpace as updateSpaceReq,
deleteSpace as deleteSpaceReq, deleteSpace as deleteSpaceReq,
getSpaceList as getSpaceListReq, getSpaceList as getSpaceListReq,
getSpaceDetail as getSpaceDetailReq getSpaceDetail as getSpaceDetailReq,
createGroup,
getGroupList as getGroupListReq,
updateGroup as updateGroupReq,
deleteGroup as deleteGroupReq
} from '@/management/api/space' } from '@/management/api/space'
import { SpaceType } from '@/management/utils/workSpace' import { GroupState, MenuType } from '@/management/utils/workSpace'
import { type SpaceDetail, type SpaceItem, type IWorkspace } from '@/management/utils/workSpace' import { type SpaceDetail, type SpaceItem, type IWorkspace, type IGroup, type GroupItem, } from '@/management/utils/workSpace'
import { useSurveyListStore } from './surveyList' import { useSurveyListStore } from './surveyList'
@ -24,16 +29,18 @@ export const useWorkSpaceStore = defineStore('workSpace', () => {
{ {
icon: 'icon-wodekongjian', icon: 'icon-wodekongjian',
name: '我的空间', name: '我的空间',
id: SpaceType.Personal id: MenuType.PersonalGroup,
children: []
}, },
{ {
icon: 'icon-tuanduikongjian', icon: 'icon-tuanduikongjian',
name: '团队空间', name: '团队空间',
id: SpaceType.Group, id: MenuType.SpaceGroup,
children: [] children: []
} }
]) ])
const spaceType = ref(SpaceType.Personal) const menuType = ref(MenuType.PersonalGroup)
const groupId = ref('')
const workSpaceId = ref('') const workSpaceId = ref('')
const spaceDetail = ref<SpaceDetail | null>(null) const spaceDetail = ref<SpaceDetail | null>(null)
const workSpaceList = ref<SpaceItem[]>([]) const workSpaceList = ref<SpaceItem[]>([])
@ -50,7 +57,8 @@ export const useWorkSpaceStore = defineStore('workSpace', () => {
const workSpace = list.map((item: SpaceDetail) => { const workSpace = list.map((item: SpaceDetail) => {
return { return {
id: item._id, id: item._id,
name: item.name name: item.name,
total: item.surveyTotal
} }
}) })
workSpaceList.value = list workSpaceList.value = list
@ -78,12 +86,19 @@ export const useWorkSpaceStore = defineStore('workSpace', () => {
} }
} }
function changeSpaceType(id: SpaceType) { function changeMenuType(id: MenuType) {
spaceType.value = id menuType.value = id
} }
function changeWorkSpace(id: string) { function changeWorkSpace(id: string) {
workSpaceId.value = id workSpaceId.value = id
groupId.value = ''
surveyListStore.resetSearch()
}
function changeGroup(id: string) {
groupId.value = id
workSpaceId.value = ''
surveyListStore.resetSearch() surveyListStore.resetSearch()
} }
@ -127,20 +142,129 @@ export const useWorkSpaceStore = defineStore('workSpace', () => {
spaceDetail.value = data spaceDetail.value = data
} }
// 分组
const groupList = ref<GroupItem[]>([])
const groupAllList = ref<IGroup[]>([])
const groupListTotal = ref(0)
const groupDetail = ref<GroupItem | null>(null)
async function addGroup(params: IGroup) {
const { name } = params
const res: any = await createGroup({ name })
if (res.code === CODE_MAP.SUCCESS) {
ElMessage.success('添加成功')
} else {
ElMessage.error('createGroup code err' + res.errmsg)
}
}
async function updateGroup(params: Required<IGroup>) {
const { _id, name } = params
const res: any = await updateGroupReq({ _id, name })
if (res?.code === CODE_MAP.SUCCESS) {
ElMessage.success('更新成功')
} else {
ElMessage.error(res?.errmsg)
}
}
async function getGroupList(params = { curPage: 1 }) {
try {
const res: any = await getGroupListReq(params)
if (res.code === CODE_MAP.SUCCESS) {
const { list, allList, total, notTotal } = res.data
let allTotal = notTotal
const group = list.map((item: GroupItem) => {
allTotal += item.surveyTotal
return { return {
id: item._id,
name: item.name,
total: item.surveyTotal,
}
})
group.unshift({
id: GroupState.All,
name: '全部' ,
total: allTotal
}, {
id: GroupState.Not,
name: '未分组' ,
total: notTotal
})
allList.unshift({
_id: '',
name: '未分组'
})
groupList.value = list
groupListTotal.value = total
spaceMenus.value[0].children = group
groupAllList.value = allList
} else {
ElMessage.error('getGroupList' + res.errmsg)
}
} catch (err) {
ElMessage.error('getGroupList' + err)
}
}
function getGroupDetail(id: string) {
try {
const data = groupList.value.find((item: GroupItem) => item._id === id)
if(data != undefined) {
groupDetail.value = data
} else {
ElMessage.error('groupDetail 未找到分组')
}
} catch (err) {
ElMessage.error('groupDetail' + err)
}
}
function setGroupDetail(data: null | GroupItem) {
groupDetail.value = data
}
async function deleteGroup(id: string) {
try {
const res: any = await deleteGroupReq(id)
if (res.code === CODE_MAP.SUCCESS) {
ElMessage.success('删除成功')
} else {
ElMessage.error(res.errmsg)
}
} catch (err: any) {
ElMessage.error(err)
}
}
return {
menuType,
spaceMenus, spaceMenus,
spaceType, groupId,
workSpaceId, workSpaceId,
spaceDetail, spaceDetail,
workSpaceList, workSpaceList,
workSpaceListTotal, workSpaceListTotal,
getSpaceList, getSpaceList,
getSpaceDetail, getSpaceDetail,
changeSpaceType, changeMenuType,
changeWorkSpace, changeWorkSpace,
changeGroup,
addSpace, addSpace,
deleteSpace, deleteSpace,
updateSpace, updateSpace,
setSpaceDetail setSpaceDetail,
groupList,
groupAllList,
groupListTotal,
groupDetail,
addGroup,
updateGroup,
getGroupList,
getGroupDetail,
setGroupDetail,
deleteGroup
} }
}) })

View File

@ -7,9 +7,15 @@ export interface MenuItem {
id: string id: string
name: string name: string
icon?: string icon?: string
total?: Number
children?: MenuItem[] children?: MenuItem[]
} }
export type IGroup = {
_id?: string
name: string
}
export type IWorkspace = { export type IWorkspace = {
_id?: string _id?: string
name: string name: string
@ -29,6 +35,7 @@ export interface SpaceDetail {
name: string name: string
currentUserId?: string currentUserId?: string
description: string description: string
surveyTotal: number
members: IMember[] members: IMember[]
} }
@ -49,16 +56,30 @@ export interface ICollaborator {
permissions: Array<number> permissions: Array<number>
} }
export enum SpaceType { export type GroupItem = {
Personal = 'personal', _id: string,
Group = 'group', name: string,
Teamwork = 'teamwork' createdAt: string
updatedAt?: string
ownerId: string
surveyTotal: number
} }
export enum MenuType {
PersonalGroup = 'personalGroup',
SpaceGroup = 'spaceGroup',
}
export enum UserRole { export enum UserRole {
Admin = 'admin', Admin = 'admin',
Member = 'user' Member = 'user'
} }
export enum GroupState {
All = 'all',
Not = 'nogrouped'
}
// 定义角色标签映射对象 // 定义角色标签映射对象
export const roleLabels: Record<UserRole, string> = { export const roleLabels: Record<UserRole, string> = {
[UserRole.Admin]: '管理员', [UserRole.Admin]: '管理员',

View File

@ -1,147 +0,0 @@
<template>
<el-dialog
v-model="whiteVisible"
title="验证"
:show-close="false"
class="verify-white-wrap"
width="315"
:close-on-press-escape="false"
:close-on-click-modal="false"
align-center
>
<template #header>
<div class="verify-white-head">
<div class="verify-white-title">验证</div>
<div v-if="whitelistTip" class="verify-white-tips">{{ whitelistTip }}</div>
</div>
</template>
<div class="verify-white-body">
<el-input
v-if="isPwd"
v-model="state.password"
class="wd255 mb16"
placeholder="请输入6位字符串类型访问密码"
/>
<el-input
v-if="isValue"
v-model="state.value"
class="wd255 mb16"
:placeholder="placeholder"
/>
<div class="submit-btn" @click="handleSubmit">验证并开始答题</div>
</div>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import { validate } from '../api/survey'
import { useSurveyStore } from '../stores/survey'
const whiteVisible = ref(false)
const surveyStore = useSurveyStore()
const state = reactive({
password: '',
value: '',
is_submit: false
})
const baseConf = computed(() => store.state.baseConf || {})
const isPwd = computed(() => baseConf.value.passwordSwitch)
const whitelistType = computed(() => baseConf.value.whitelistType)
const memberType = computed(() => baseConf.value.memberType)
const whitelistTip = computed(() => baseConf.value.whitelistTip)
const surveyPath = computed(() => store.state?.surveyPath || '')
const isValue = computed(() => {
if (!whitelistType.value) return false
return whitelistType.value != 'ALL'
})
const placeholder = computed(() => {
if (whitelistType.value == 'MEMBER') {
return '请输入用户名'
}
if (memberType.value == 'MOBILE') {
return '请输入手机号'
}
if (memberType.value == 'EMAIL') {
return '请输入邮箱'
}
return ''
})
const handleSubmit = async () => {
if (state.is_submit) return
const params = {
surveyPath: surveyPath.value
}
if (isValue.value) {
params.whitelist = state.value
}
if (isPwd.value) {
params.password = state.password
}
const res = await validate(params)
if (res.code != 200) {
ElMessage.error(res.errmsg || '验证失败')
return
}
whiteVisible.value = false
surveyStore.setWhiteData(params)
}
watch(
() => baseConf.value,
() => {
if (whiteVisible.value) return
if (isValue.value || isPwd.value) {
whiteVisible.value = true
}
}
)
</script>
<style lang="scss" scoped>
.verify-white-wrap {
.verify-white-body {
padding: 0 14px;
}
.verify-white-head {
padding: 0 14px;
margin-bottom: 8px;
margin-top: 2px;
}
.mb16 {
margin-bottom: 16px;
}
.verify-white-tips {
text-align: center;
margin-top: 8px;
font-size: 14px;
color: #92949d;
}
.verify-white-title {
font-size: 16px;
color: #292a36;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
.submit-btn {
background: #faa600;
border-radius: 2px;
width: 255px;
height: 32px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
margin-top: 4px;
margin-bottom: 14px;
}
}
</style>

View File

@ -16,13 +16,17 @@
> >
重新填写 重新填写
</router-link> </router-link>
<a v-if="showJumpButton" :href="jumpConfig.link" class="jump-btn">
{{ jumpConfig.buttonText }}
</a>
</div> </div>
<LogoIcon :logo-conf="logoConf" :readonly="true" /> <LogoIcon :logo-conf="logoConf" :readonly="true" />
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, ref, watchEffect } from 'vue'
import { useSurveyStore } from '../stores/survey' import { useSurveyStore } from '../stores/survey'
// @ts-ignore // @ts-ignore
import communalLoader from '@materials/communals/communalLoader.js' import communalLoader from '@materials/communals/communalLoader.js'
@ -37,6 +41,20 @@ const successMsg = computed(() => {
const msgContent = (surveyStore?.submitConf as any)?.msgContent || {} const msgContent = (surveyStore?.submitConf as any)?.msgContent || {}
return msgContent?.msg_200 || '提交成功' return msgContent?.msg_200 || '提交成功'
}) })
const jumpConfig = computed(() => {
return (surveyStore?.submitConf as any)?.jumpConfig || {}
})
const showJumpButton = ref(false)
watchEffect(() => {
const { jumpConfig } = (surveyStore?.submitConf || {}) as any
if (jumpConfig?.type === 'link' && jumpConfig?.link) {
window.location.href = jumpConfig.link
}
showJumpButton.value = jumpConfig?.type === 'button' && jumpConfig?.buttonText
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/render/styles/variable.scss'; @import '@/render/styles/variable.scss';
@ -87,5 +105,20 @@ const successMsg = computed(() => {
text-decoration: underline; text-decoration: underline;
display: block; display: block;
} }
.jump-btn {
background: var(--primary-color);
width: 90%;
border-radius: 0.08rem;
padding: 0.2rem 0;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.3rem;
font-weight: 500;
margin: 0.5rem auto 0;
border: none;
}
} }
</style> </style>