feat: 问卷空间协作能力前端 (#246)

This commit is contained in:
dayou 2024-05-30 22:11:11 +08:00 committed by GitHub
parent f9d75962ed
commit 404ba360b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 2279 additions and 478 deletions

View File

@ -11,5 +11,11 @@ module.exports = {
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'no-empty-pattern': 'off',
"vue/multi-word-component-names": ["error", {
"ignores": ["index"]
}]
}
}

5
web/components.d.ts vendored
View File

@ -19,6 +19,9 @@ declare module 'vue' {
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
@ -27,6 +30,7 @@ declare module 'vue' {
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
@ -44,6 +48,7 @@ declare module 'vue' {
IEpDelete: typeof import('~icons/ep/delete')['default']
IEpLoading: typeof import('~icons/ep/loading')['default']
IEpMinus: typeof import('~icons/ep/minus')['default']
IEpMore: typeof import('~icons/ep/more')['default']
IEpPlus: typeof import('~icons/ep/plus')['default']
IEpQuestionFilled: typeof import('~icons/ep/question-filled')['default']
IEpRank: typeof import('~icons/ep/rank')['default']

View File

@ -0,0 +1,75 @@
import axios from './base'
// 空间
export const createSpace = ({ name, description, members }: any) => {
return axios.post('/workspace', { name, description, members })
}
export const updateSpace = ({ workspaceId, name, description, members }: any) => {
return axios.post(`/workspace/${workspaceId}`, { name, description, members })
}
export const getSpaceList = () => {
return axios.get('/workspace')
}
export const getSpaceDetail = (workspaceId: string) => {
return axios.get(`/workspace/${workspaceId}`)
}
export const deleteSpace = (workspaceId: string) => {
return axios.delete(`/workspace/${workspaceId}`)
}
export const getUserList = (username: string) => {
return axios.get(`/user/getUserList`, {
params: {
username
}
})
}
// 协作权限列表
export const getPermissionList = () => {
return axios.get('collaborator/getPermissionList')
}
export const saveCollaborator = ({ surveyId, collaborators }: any) => {
return axios.post('collaborator/batchSave', {
surveyId,
collaborators
})
}
// 添加协作人
export const addCollaborator = ({ surveyId, userId, permissions }: any) => {
return axios.post('collaborator', {
surveyId,
userId,
permissions
})
}
// 更新问卷协作信息
export const updateCollaborator = ({ surveyId, userId, permissions }: any) => {
return axios.post('collaborator/changeUserPermission', {
surveyId,
userId,
permissions
})
}
// 获取问卷协作信息
export const getCollaborator = (surveyId: string) => {
return axios.get(`collaborator`, {
params: {
surveyId
}
})
}
// 获取问卷协作权限
export const getCollaboratorPermissions = (surveyId: string) => {
return axios.get(`collaborator/permissions`, {
params: {
surveyId
}
})
}

View File

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

View File

@ -1,19 +1,40 @@
<template>
<div class="nav">
<LogoIcon />
<RouterLink v-for="(tab, index) in tabs" :key="index" class="tab-btn" :to="tab.to" replace>
<template v-for="(tab, index) in tabs" :key="tab.text + index">
<router-link :to="tab.to" v-slot="{ isActive }">
<div
:class="[
'tab-btn',
(['QuestionEditIndex', 'QuestionEditSetting', 'QuestionSkinSetting'].includes(
route.name
) &&
tab.to.name === 'QuestionEditIndex') ||
isActive
? 'router-link-active'
: ''
]"
>
<div class="icon">
<i class="iconfont" :class="tab.icon"></i>
</div>
<p>{{ tab.text }}</p>
</RouterLink>
</div>
</router-link>
</template>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useStore } from 'vuex'
import { useRoute } from 'vue-router'
const route = useRoute()
import LogoIcon from './LogoIcon.vue'
import { SurveyPermissions } from '@/management/utils/types/workSpace.ts'
const store = useStore()
const tabs = [
const tabArr = [
{
text: '编辑问卷',
icon: 'icon-bianji',
@ -36,6 +57,19 @@ const tabs = [
}
}
]
const tabs = ref([])
onMounted(async () => {
await store.dispatch('fetchCooperPermissions', route.params.id)
//
if (store.state.cooperPermissions.includes(SurveyPermissions.SurveyManage)) {
tabs.value.push(tabArr[0])
tabs.value.push(tabArr[1])
}
//
if (store.state.cooperPermissions.includes(SurveyPermissions.DataManage)) {
tabs.value.push(tabArr[2])
}
})
</script>
<style lang="scss" scoped>
.nav {

View File

@ -38,6 +38,7 @@
<script setup lang="ts">
import { ref, reactive, computed, toRefs } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
@ -78,7 +79,7 @@ const checkForm = (fn: Function) => {
}
const router = useRouter()
const store = useStore()
const submit = () => {
if (!state.canSubmit) {
return
@ -89,10 +90,14 @@ const submit = () => {
return
}
state.canSubmit = false
const res:any = await createSurvey({
const payload: any = {
surveyType: selectType,
...state.form
})
}
if (store.state.list.workSpaceId) {
payload.workspaceId = store.state.list.workSpaceId
}
const res: any = await createSurvey(payload)
if (res?.code === 200 && res?.data?.id) {
const id = res.data.id
router.push({

View File

@ -30,7 +30,7 @@ const onBack = () => {
const toHomePage = () => {
router.push({
name: 'Home'
name: 'survey'
})
}
</script>

View File

@ -73,5 +73,3 @@ const hideFullTitle = () => {
white-space: nowrap;
}
</style>

View File

@ -49,7 +49,7 @@
</div>
</template>
<script setup lang="ts">
import { defineProps, computed, inject, ref, type ComputedRef } from 'vue'
import { computed, inject, ref, type ComputedRef } from 'vue'
import { ConditionNode, RuleNode } from '@/common/logicEngine/RuleBuild'
import { qAbleList } from '@/management/utils/constant.js'
import { cleanRichText } from '@/common/xss'

View File

@ -29,18 +29,18 @@ import MaterialGroup from '@/management/pages/edit/components/MaterialGroup.vue'
import { useStore } from 'vuex'
import communalLoader from '@materials/communals/communalLoader.js'
const HeaderContent = ()=>communalLoader.loadComponent('HeaderContent')
const MainTitle = ()=>communalLoader.loadComponent('MainTitle')
const SubmitButton = ()=>communalLoader.loadComponent('SubmitButton')
const LogoIcon = ()=>communalLoader.loadComponent('LogoIcon')
const HeaderContent = () => communalLoader.loadComponent('HeaderContent')
const MainTitle = () => communalLoader.loadComponent('MainTitle')
const SubmitButton = () => communalLoader.loadComponent('SubmitButton')
const LogoIcon = () => communalLoader.loadComponent('LogoIcon')
export default defineComponent({
components: {
MaterialGroup,
HeaderContent:HeaderContent(),
MainTitle:MainTitle(),
SubmitButton:SubmitButton(),
LogoIcon:LogoIcon()
HeaderContent: HeaderContent(),
MainTitle: MainTitle(),
SubmitButton: SubmitButton(),
LogoIcon: LogoIcon()
},
setup() {
const store = useStore()

View File

@ -5,17 +5,16 @@
<TextSelect
v-for="item in Object.keys(selectOptionsDict)"
:key="item"
:effect-fun="onSelectChange"
:effect-key="item"
:options="selectOptionsDict[item]"
:value="selectValueMap[item]"
@change="(value) => onSelectChange(item, value)"
/>
</div>
<div class="search">
<TextButton
v-for="item in Object.keys(buttonOptionsDict)"
:key="item"
:effect-fun="onButtonChange"
:effect-key="item"
@change="(value) => onButtonChange(item, value)"
:option="buttonOptionsDict[item]"
:icon="
buttonOptionsDict[item].icons.find(
@ -27,6 +26,7 @@
<TextSearch placeholder="请输入问卷标题" :value="searchVal" @search="onSearchText" />
</div>
</div>
<div class="list-wrapper" v-if="total">
<el-table
v-if="total"
ref="multipleListTable"
@ -53,7 +53,11 @@
>
<template #default="scope">
<template v-if="field.comp">
<component :is="field.comp" type="table" :value="scope.row" />
<component
:is="currentComponent(field.comp)"
type="table"
:value="unref(scope.row)"
/>
</template>
<template v-else>
<span class="cell-span">{{ scope.row[field.key] }}</span>
@ -61,19 +65,19 @@
</template>
</el-table-column>
<el-table-column label="操作" :width="300" class-name="table-options" fixed="right">
<el-table-column label="操作" :width="230" class-name="table-options" fixed="right">
<template #default="scope">
<ToolBar
:data="scope.row"
type="list"
:tools="getToolConfig(scope.row)"
:tool-width="50"
@on-delete="onDelete"
@on-modify="onModify"
@click="handleClick"
/>
</template>
</el-table-column>
</el-table>
</div>
<div class="list-pagination" v-if="total">
<el-pagination
@ -95,10 +99,14 @@
:question-info="questionInfo"
@on-close-codify="onCloseModify"
/>
<CooperModify :modifyId="cooperId" :visible="cooperModify" @on-close-codify="onCooperClose" />
</div>
</template>
<script>
<script setup>
import { ref, computed, unref } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
import { get, map } from 'lodash-es'
import { ElMessage, ElMessageBox } from 'element-plus'
@ -114,8 +122,7 @@ moment.locale('zh-cn')
import EmptyIndex from '@/management/components/EmptyIndex.vue'
import { CODE_MAP } from '@/management/api/base'
import { QOP_MAP } from '@/management/utils/constant'
import { getSurveyList, deleteSurvey } from '@/management/api/survey'
import { deleteSurvey } from '@/management/api/survey'
import ModifyDialog from './ModifyDialog.vue'
import TagModule from './TagModule.vue'
import StateModule from './StateModule.vue'
@ -123,6 +130,9 @@ import ToolBar from './ToolBar.vue'
import TextSearch from './TextSearch.vue'
import TextSelect from './TextSelect.vue'
import TextButton from './TextButton.vue'
import CooperModify from './CooperModify.vue'
import { SurveyPermissions } from '@/management/utils/types/workSpace'
import {
fieldConfig,
noListDataConfig,
@ -131,56 +141,78 @@ import {
buttonOptionsDict
} from '../config'
export default {
name: 'BaseList',
data() {
return {
fields: ['type', 'title', 'remark', 'owner', 'state', 'createDate', 'updateDate'],
showModify: false,
modifyType: '',
loading: false,
noListDataConfig,
noSearchDataConfig,
questionInfo: {},
total: 0,
data: [],
currentPage: 1,
searchVal: '',
selectOptionsDict,
selectValueMap: {
surveyType: '',
'curStatus.status': ''
const store = useStore()
const router = useRouter()
const props = defineProps({
loading: {
type: Boolean,
default: false
},
buttonOptionsDict,
buttonValueMap: {
'curStatus.date': '',
createDate: -1
data: {
type: Array,
default: () => []
},
total: {
type: Number,
default: 0
}
})
const emit = defineEmits(['reflush'])
const fields = ['type', 'title', 'remark', 'owner', 'state', 'createDate', 'updateDate']
const showModify = ref(false)
const modifyType = ref('')
const questionInfo = ref({})
const currentPage = ref(1)
const searchVal = computed(() => {
return store.state.list.searchVal
})
const selectValueMap = computed(() => {
return store.state.list.selectValueMap
})
const buttonValueMap = computed(() => {
return store.state.list.buttonValueMap
})
const currentComponent = computed(() => {
return (componentName) => {
switch (componentName) {
case 'TagModule':
return TagModule
case 'StateModule':
return StateModule
default:
return null
}
}
},
computed: {
fieldList() {
const fieldInfo = map(this.fields, (f) => {
})
const fieldList = computed(() => {
return map(fields, (f) => {
return get(fieldConfig, f, null)
})
return fieldInfo
},
dataList() {
return this.data.map((item) => {
})
const data = computed(() => {
return props.data
})
const total = computed(() => {
return props.total
})
const dataList = computed(() => {
return data.value.map((item) => {
return {
...item,
'curStatus.date': item.curStatus.date
}
})
},
filter() {
})
const filter = computed(() => {
return [
{
comparator: '',
condition: [
{
field: 'title',
value: this.searchVal,
value: searchVal.value,
comparator: '$regex'
}
]
@ -190,7 +222,7 @@ export default {
condition: [
{
field: 'curStatus.status',
value: this.selectValueMap['curStatus.status']
value: selectValueMap.value['curStatus.status']
}
]
},
@ -199,14 +231,14 @@ export default {
condition: [
{
field: 'surveyType',
value: this.selectValueMap.surveyType
value: selectValueMap.value.surveyType
}
]
}
]
},
order() {
const formatOrder = Object.entries(this.buttonValueMap)
})
const order = computed(() => {
const formatOrder = Object.entries(buttonValueMap.value)
.filter(([, effectValue]) => effectValue)
.reduce((prev, item) => {
const [effectKey, effectValue] = item
@ -214,54 +246,35 @@ export default {
return prev
}, [])
return JSON.stringify(formatOrder)
}
},
created() {
this.init()
},
methods: {
async init() {
this.loading = true
try {
const filter = JSON.stringify(
this.filter.filter((item) => {
})
const workSpaceId = computed(() => {
return store.state.list.workSpaceId
})
const onReflush = async () => {
const filterString = JSON.stringify(
filter.value.filter((item) => {
return item.condition[0].value
})
)
const res = await getSurveyList({
curPage: this.currentPage,
filter,
order: this.order
})
this.loading = false
if (res.code === CODE_MAP.SUCCESS) {
this.total = res.data.count
this.data = res.data.data
} else {
ElMessage.error(res.errmsg)
let params = {
curPage: currentPage.value,
filter: filterString,
order: order.value
}
} catch (error) {
ElMessage.error(error)
this.loading = false
if (workSpaceId.value) {
params.workspaceId = workSpaceId.value
}
},
getStatus(data) {
return get(data, 'curStatus.status', 'new')
},
getToolConfig() {
const funcList = [
emit('reflush', params)
}
const getToolConfig = (row) => {
let funcList = []
const permissionsBtn = [
{
key: QOP_MAP.EDIT,
label: '修改'
},
{
key: 'analysis',
label: '数据'
},
{
key: 'release',
label: '投放'
},
{
key: 'delete',
label: '删除',
@ -271,11 +284,108 @@ export default {
key: QOP_MAP.COPY,
label: '复制',
icon: 'icon-shanchu'
},
{
key: 'analysis',
label: '数据'
},
{
key: 'release',
label: '投放'
},
{
key: 'cooper',
label: '协作'
}
]
return funcList
if (!store.state.list.workSpaceId) {
if (!row.isCollaborated) {
//
funcList = funcList.concat(permissionsBtn)
} else {
if (row.currentPermissions.includes(SurveyPermissions.DataManage)) {
//
funcList.push({
key: 'analysis',
label: '数据'
})
}
if (row.currentPermissions.includes(SurveyPermissions.SurveyManage)) {
//
funcList.push(
{
key: QOP_MAP.EDIT,
label: '修改'
},
async onDelete(row) {
{
key: 'delete',
label: '删除',
icon: 'icon-shanchu'
},
{
key: QOP_MAP.COPY,
label: '复制',
icon: 'icon-shanchu'
},
{
key: 'release',
label: '投放'
}
)
}
if (row.currentPermissions.includes(SurveyPermissions.CollaboratorManage)) {
//
funcList.push({
key: 'cooper',
label: '协作'
})
}
}
} else {
//
permissionsBtn.splice(-1)
funcList = permissionsBtn
}
const order = ['edit', 'analysis', 'release', 'delete', 'copy', 'cooper']
const result = funcList.sort((a, b) => order.indexOf(a.key) - order.indexOf(b.key))
return result
}
const handleClick = (key, data) => {
switch (key) {
case QOP_MAP.EDIT:
onModify(data, QOP_MAP.EDIT)
return
case QOP_MAP.COPY:
onModify(data, QOP_MAP.COPY)
return
case 'analysis':
router.push({
name: 'analysisPage',
params: {
id: data._id
}
})
return
case 'release':
router.push({
name: 'publish',
params: {
id: data._id
}
})
return
case 'delete':
onDelete(data)
return
case 'cooper':
onCooper(data)
return
default:
return
}
}
const onDelete = async (row) => {
try {
await ElMessageBox.confirm('是否确认删除?', '提示', {
confirmButtonText: '确定',
@ -290,64 +400,60 @@ export default {
const res = await deleteSurvey(row._id)
if (res.code === CODE_MAP.SUCCESS) {
ElMessage.success('删除成功')
this.init()
onReflush()
} else {
ElMessage.error(res.errmsg || '删除失败')
}
},
handleCurrentChange(current) {
this.currentPage = current
this.init()
},
onModify(data, type = QOP_MAP.EDIT) {
this.showModify = true
this.modifyType = type
this.questionInfo = data
},
onCloseModify(type) {
this.showModify = false
this.questionInfo = {}
}
const handleCurrentChange = (current) => {
currentPage.value = current
onReflush()
}
const onModify = (data, type = QOP_MAP.EDIT) => {
showModify.value = true
modifyType.value = type
questionInfo.value = data
}
const onCloseModify = (type) => {
showModify.value = false
questionInfo.value = {}
if (type === 'update') {
this.init()
onReflush()
}
},
onRowClick(row) {
this.$router.push({
}
const onRowClick = (row) => {
router.push({
name: 'QuestionEditIndex',
params: {
id: row._id
}
})
},
onSearchText(e) {
this.searchVal = e
this.currentPage = 1
this.init()
},
onSelectChange(selectValue, selectKey) {
this.selectValueMap[selectKey] = selectValue
this.currentPage = 1
this.init()
},
onButtonChange(effectValue, effectKey) {
this.buttonValueMap = {
'curStatus.date': '',
createDate: ''
}
this.buttonValueMap[effectKey] = effectValue
this.init()
}
},
components: {
EmptyIndex,
ModifyDialog,
TagModule,
ToolBar,
TextSearch,
TextSelect,
TextButton,
StateModule
}
}
const onSearchText = (e) => {
store.commit('list/setSearchVal', e)
currentPage.value = 1
onReflush()
}
const onSelectChange = (selectKey, selectValue) => {
store.commit('list/changeSelectValueMap', { key: selectKey, value: selectValue })
// selectValueMap.value[selectKey] = selectValue
currentPage.value = 1
onReflush()
}
const onButtonChange = (effectKey, effectValue) => {
store.commit('list/reserButtonValueMap')
store.commit('list/changeButtonValueMap', { key: effectKey, value: effectValue })
onReflush()
}
const cooperModify = ref(false)
const cooperId = ref('')
const onCooper = async (row) => {
cooperId.value = row._id
cooperModify.value = true
}
const onCooperClose = () => {
cooperModify.value = false
}
</script>
@ -365,11 +471,14 @@ export default {
display: flex;
}
}
.list-wrapper {
padding: 10px 20px;
background: #fff;
.list-table {
min-height: 620px;
padding: 10px 20px;
}
}
.list-pagination {
margin-top: 20px;
:deep(.el-pagination) {

View File

@ -0,0 +1,192 @@
<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
:disabled="formDisabled"
>
<el-form-item label="添加协作者" prop="members">
<MemberSelect
:multiple="true"
:members="formModel.members"
:options="cooperOptions"
@select="handleMemberSelect"
@change="handleMembersChange"
>
</MemberSelect>
</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" v-if="!formDisabled">
确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref, shallowRef, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import MemberSelect from './MemberSelect.vue'
import { getPermissionList, getCollaborator, saveCollaborator } from '@/management/api/space'
import { type IMember, SurveyPermissions } from '@/management/utils/types/workSpace'
import { CODE_MAP } from '@/management/api/base'
const emit = defineEmits(['on-close-codify', 'onFocus', 'change', 'blur'])
const props = withDefaults(
defineProps<{
modifyId: string
visible: boolean
}>(),
{
modifyId: '',
visible: false
}
)
const ruleForm = shallowRef<any>(null)
const formTitle = ref('协作管理')
const cooperOptions = ref([])
onMounted(async () => {
const res: any = await getPermissionList()
if (res.code === CODE_MAP.SUCCESS) {
cooperOptions.value = res.data.map((item: any) => {
return {
label: item.name,
value: item.value
}
})
} else {
ElMessage.error(res.errmsg || '获取权限信息失败')
}
})
const formModel = ref({
members: [] as IMember[]
})
watch(
() => props.visible,
async (val: boolean) => {
if (val && props.modifyId) {
try {
const res: any = await getCollaborator(props.modifyId)
if (res.code === CODE_MAP.SUCCESS) {
formModel.value.members = res.data?.map((item: any) => {
return {
_id: item._id,
userId: item.userId,
username: item.username,
role: item.permissions
}
})
} else {
formModel.value.members = []
ElMessage.error(res.errmsg || '获取协作信息失败')
}
} catch (err) {
ElMessage.error('获取协作信息接口请求错误')
}
}
}
)
const rules = {
members: [
{
trigger: 'change',
validator: (rule: any, value: IMember[], callback: Function) => {
if (value.length === 0) {
callback('请至少添加一名协作者')
}
if (value.filter((item: IMember) => !item.role.length).length) {
callback('请设置协作者对应权限')
}
if (
value.filter(
(item: IMember) =>
item.role.length === 1 && item.role[0] === SurveyPermissions.CollaboratorManage
).length
) {
callback('不能单独设置协作者管理')
}
callback()
}
}
]
}
const formDisabled = computed(() => {
return false
})
const onClose = () => {
emit('on-close-codify')
}
const onConfirm = async () => {
ruleForm.value?.validate(async (valid: boolean) => {
if (valid) {
try {
const collaborators = formModel.value.members.map((i: any) => {
const collaborator = {
userId: i.userId,
permissions: i.role
}
if (i._id) {
;(collaborator as any)._id = i._id
}
return collaborator
})
const res: any = await saveCollaborator({
surveyId: props.modifyId,
collaborators
})
if (res.code === CODE_MAP.SUCCESS) {
ElMessage.success('操作成功')
emit('on-close-codify')
} else {
ElMessage.error(res.errmsg || '协作管理接口调用失败')
}
} catch (err) {
ElMessage.error('createSpace status err' + err)
}
} else {
return false
}
})
}
const handleMemberSelect = (val: string, label: string) => {
formModel.value.members.push({
userId: val,
username: label,
role: [
SurveyPermissions.SurveyManage,
SurveyPermissions.DataManage,
SurveyPermissions.CollaboratorManage
]
})
}
const handleMembersChange = (val: IMember[]) => {
formModel.value.members = val
}
</script>
<style lang="scss" rel="lang/scss" scoped>
.base-form-root {
padding: 20px;
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<div class="list-wrapper" v-if="list.length">
<div class="content" v-for="(item, index) in list" :key="item.userId">
<div>{{ item.username }}</div>
<div class="operation">
<OperationSelect
:options="options"
v-model="item.role"
:multiple="multiple"
@customClick="() => handleRemove(index)"
:disabled="item.userId === currentUserId"
></OperationSelect>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, withDefaults } from 'vue'
import { useStore } from 'vuex'
import { type IMember, type ListItem } from '@/management/utils/types/workSpace'
import OperationSelect from './OperationSelect.vue'
const store = useStore()
const props = withDefaults(
defineProps<{
members: IMember[]
options: ListItem[]
multiple: boolean
}>(),
{
members: () => [],
options: () => [],
multiple: false
}
)
const emit = defineEmits(['change'])
const list = computed({
get() {
return props.members
},
set(value) {
emit('change', value)
}
})
const currentUserId = computed(() => {
return store.state.list.spaceDetail?.currentUserId
})
const handleRemove = (index: number) => {
list.value.splice(index, 1)
}
</script>
<style lang="scss" scoped>
.list-wrapper {
width: 100%;
height: 200px;
border: 1px solid #ccc;
margin-top: 8px;
overflow: auto;
.head {
display: flex;
justify-content: space-between;
border-bottom: 1px solid #ccc;
padding: 0 20px;
}
.content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
.operation {
display: flex;
}
}
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<div class="wrapper">
<el-select-v2
v-model="value"
filterable
remote
:remote-method="remoteMethod"
clearable
:options="selectOptions"
:loading="loading"
placeholder="请输入账号名搜索"
@change="handleSelect"
/>
<MemberList
:members="members"
:options="options"
@change="handleMemberChange"
:multiple="multiple"
>
</MemberList>
</div>
</template>
<script lang="ts" setup>
import { ref, withDefaults } from 'vue'
import { useStore } from 'vuex'
import MemberList from './MemberList.vue'
import { getUserList } from '@/management/api/space'
import {
type IMember,
type ListItem,
type UserRole,
roleLabels
} from '@/management/utils/types/workSpace'
import { CODE_MAP } from '@/management/api/base'
const store = useStore()
const props = withDefaults(
defineProps<{
members?: IMember[]
options?: ListItem[]
multiple?: boolean
}>(),
{
members: () => [],
options: () => {
return Object.keys(roleLabels).map((key) => ({
label: roleLabels[key as UserRole],
value: key
}))
},
multiple: false
}
)
const emit = defineEmits(['select', 'change'])
const value = ref('')
const selectOptions = ref<ListItem[]>([])
const loading = ref(false)
const remoteMethod = async (query: string) => {
if (query !== '') {
loading.value = true
const res: any = await getUserList(query)
if (res.code === CODE_MAP.SUCCESS) {
selectOptions.value = res.data.map((item: any) => {
//
const currentUser = item.username === store.state.user.userInfo.username
return {
value: item.userId,
label: item.username,
disabled: props.members.map((item) => item.userId).includes(item.userId) || currentUser
}
})
loading.value = false
}
} else {
selectOptions.value = []
}
}
const handleSelect = (val: string) => {
value.value = ''
emit('select', val, selectOptions.value?.find((item) => item.value === val)?.label)
}
const handleMemberChange = (val: any) => {
emit('change', val)
}
</script>
<style lang="scss" scoped>
.wrapper {
width: 100%;
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<el-button text type="primary" ref="buttonRef" v-click-outside="onClickOutside">
<i-ep-more />
</el-button>
<el-popover
ref="popoverRef"
:width="width"
:virtual-ref="buttonRef"
placement="top"
trigger="hover"
popper-class="more-tool_popper"
:popper-options="{ boundariesElement: '.more-tool_root', removeOnDestroy: true }"
virtual-triggering
>
<div :class="[type === 'card' ? 'card-tool_more_ul' : 'table-tool_more_ul', 'popper_body']">
<div
v-for="t in tools"
:key="t.key"
:class="[type === 'card' ? 'card-tool-li_base' : 'table-tool-li_base', 'popper_con']"
@click="call(t)"
>
<span class="more_con">{{ t.label }}</span>
</div>
</div>
</el-popover>
</template>
<script setup>
import { ref, unref } from 'vue'
import { ClickOutside as vClickOutside } from 'element-plus'
defineProps({
type: String,
placement: String,
tools: Array,
width: {
type: Number,
default: 50
}
})
const emit = defineEmits(['popper', 'call'])
const buttonRef = ref()
const popoverRef = ref()
const onClickOutside = () => {
unref(popoverRef).popperRef?.delayHide?.()
}
const call = (t) => {
emit('call', {
key: t.key,
name: t.label
})
}
</script>
<style lang="scss" rel="stylesheet/scss">
.el-popover.more-tool_popper {
min-width: 80px;
padding: 8px 3px;
.popper_body {
.popper_con {
cursor: pointer;
height: 28px;
&:hover {
background: #fef6e6 100%;
span.more_con {
color: #faa600;
}
}
text-align: center;
}
.popper_con span.more_con {
min-width: 76px;
font-size: 12px;
font-weight: 400;
color: $font-color;
line-height: 28px;
font-size: 14px;
display: inline-block;
text-align: center;
// line-height: 16px;
cursor: pointer;
font-weight: 500;
}
}
}
</style>

View File

@ -0,0 +1,156 @@
<template>
<div class="operation">
<el-select
size="small"
:multiple="multiple"
v-model="value"
placeholder="请选择"
:style="{ width: `${multiple ? 226 : 100}px` }"
popper-class="custom-popper"
@change="handleChange"
class="operation-select"
:disabled="disabled"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
<template #header v-if="multiple">
<el-checkbox v-model="checkAll" :indeterminate="indeterminate" @change="handleCheckAll">
全部权限
</el-checkbox>
</template>
<template #tag v-if="multiple">
<el-tag type="primary" v-if="value.length === options.length">全部权限</el-tag>
<el-tag v-for="chose in value" :key="chose" v-else>{{ chosenLabel(chose) }}</el-tag>
</template>
<template #footer>
<el-button class="remove-btn" link type="danger" @click="handleClick">
&nbsp;&nbsp;删除</el-button
>
</template>
</el-select>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue'
import { ElMessageBox, type CheckboxValueType } from 'element-plus'
import 'element-plus/theme-chalk/src/message-box.scss'
import { type ListItem } from '@/management/utils/types/workSpace'
const props = withDefaults(
defineProps<{
multiple?: boolean
modelValue: string | string[]
options: ListItem[]
width?: number
disabled?: boolean
}>(),
{
multiple: false,
modelValue: '',
options: () => [],
width: 100,
disabled: false
}
)
const emit = defineEmits(['update:modelValue', 'change', 'customClick'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
const chosenLabel = computed(() => {
return (chose: string) => {
return props.options.find((i) => i.value === chose)?.label
}
})
const handleChange = (val: string | string[]) => {
emit('change', val)
}
const handleClick = () => {
const text = props.multiple
? '删除协作者后,用户不再有该问卷下的相关权限'
: '删除团队成员后,该成员不再有团队空间的访问权限'
ElMessageBox.confirm(text, '是否确认本次删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
emit('customClick')
})
.catch(() => {})
}
const checkAll = ref(false)
const indeterminate = ref(false)
watch(
() => props.modelValue,
(value) => {
if (props.multiple) {
//
if (value.length === 0) {
checkAll.value = false
indeterminate.value = false
} else if (value.length === props.options.length) {
checkAll.value = true
indeterminate.value = false
} else {
indeterminate.value = true
}
}
},
{ immediate: true }
)
const handleCheckAll = (val: CheckboxValueType) => {
indeterminate.value = false
if (val) {
value.value = props.options.map((_) => _.value)
} else {
value.value = []
}
}
</script>
<style lang="scss" scoped>
.operation {
:deep(.el-select__wrapper) {
border: none;
box-shadow: none;
}
:deep(.ishovering) {
border: none;
box-shadow: none;
}
:deep(.el-select__selection, .is-near) {
display: flex;
justify-content: flex-end;
}
.operation-select {
:deep(.el-select__placeholder) {
text-align: right;
}
}
}
</style>
<style lang="scss">
.custom-popper {
.el-checkbox {
display: flex;
height: 22px;
}
.el-button,
.remove-btn {
width: 100%;
justify-content: flex-start;
}
}
</style>

View File

@ -0,0 +1,130 @@
<template>
<el-menu
:default-active="SpaceType.Personal"
class="el-menu-vertical"
ref="menuRef"
@select="handleSelect"
>
<template v-for="(menu, index) in menus" :key="menu.id">
<el-menu-item
:class="[index === 0 ? 'bottom' : '', index > 2 ? 'sub-item' : 'main-item']"
:index="menu.id"
v-if="!menu.children?.length"
>
<template #title>
<div class="title-content">
<i :class="['iconfont', menu.icon]"></i>
<span>{{ menu.name }}</span>
</div>
</template>
</el-menu-item>
<el-menu-item-group v-else>
<template #title>
<el-menu-item :index="menu.id" class="sub-title main-item">
<div class="title-content">
<i :class="['iconfont', menu.icon]"></i>
<span>{{ menu.name }}</span>
</div>
</el-menu-item>
</template>
<el-menu-item v-for="item in menu.children" :key="item.id" :index="item.id">
<p>
{{ item.name }}
</p>
</el-menu-item>
</el-menu-item-group>
</template>
</el-menu>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { type MenuItem } from '@/management/utils/types/workSpace'
import { SpaceType } from '@/management/utils/types/workSpace'
const menuRef = ref()
withDefaults(
defineProps<{
menus: Array<MenuItem>
}>(),
{
menus: () => []
}
)
const emit = defineEmits(['select'])
const handleSelect = (id: string) => {
emit('select', id)
}
</script>
<style lang="scss" scoped>
.el-menu-vertical {
border: none;
width: 200px;
min-height: 400px;
height: 100%;
position: absolute;
top: 1px;
bottom: 0px;
z-index: 999;
overflow-x: hidden;
overflow-y: auto;
box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.04);
:deep(.el-menu-item) {
width: 200px;
height: 36px;
> p {
overflow: hidden;
/*文本不会换行*/
white-space: nowrap !important;
/*当文本溢出包含元素时,以省略号表示超出的文本*/
text-overflow: ellipsis;
}
&.bottom {
border-bottom: 1px solid #e3e4e8;
}
&.main-item {
// margin: 10px 0;
font-size: 16px;
font-weight: 500;
color: #292a36;
height: 48px;
}
&.sub-item {
margin: 0;
}
&.is-active {
// background-color: #F2F4F7;
background: #fef6e6 100% !important;
}
&:hover {
background-color: #f2f4f7;
}
.title-content {
display: flex;
align-items: center;
}
}
:deep(.el-menu-item-group) {
> ul {
> li {
padding-left: 40px !important;
}
}
}
:deep(.el-menu-item-group__title) {
cursor: pointer;
padding: 0 !important;
}
.sub-title {
width: 100%;
width: 100%;
}
}
.iconfont {
font-size: 16px;
margin-right: 5px;
color: #faa600 !important;
}
</style>

View File

@ -0,0 +1,171 @@
<template>
<div class="list-wrap">
<el-table
ref="multipleListTable"
class="list-table"
:data="dataList"
empty-text="暂无数据"
row-key="_id"
header-row-class-name="tableview-header"
row-class-name="tableview-row"
cell-class-name="tableview-cell"
style="width: 100%"
@row-click="onRowClick"
>
<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="tool-root">
<!-- <el-button text type="primary" class="tool-root-btn-text" :style="{ width: 50 + 'px' }" @click.stop="handleEnter(scope.row)">进入</el-button> -->
<el-button
text
type="primary"
class="tool-root-btn-text"
:style="{ width: 50 + 'px' }"
@click.stop="handleModify(scope.row._id)"
>{{ isAdmin(scope.row._id) ? '修改' : '查看' }}</el-button
>
<el-button
text
type="primary"
class="tool-root-btn-text"
:style="{ width: 50 + 'px' }"
@click.stop="handleDelete(scope.row._id)"
v-if="isAdmin(scope.row._id)"
>删除</el-button
>
</div>
</template>
</el-table-column>
</el-table>
</div>
<SpaceModify
v-if="showSpaceModify"
:type="modifyType"
:visible="showSpaceModify"
@on-close-codify="onCloseModify"
/>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { useStore } from 'vuex'
import { ElMessageBox } from 'element-plus'
import 'element-plus/theme-chalk/src/message-box.scss'
import { get, map } from 'lodash-es'
import { spaceListConfig } from '../config'
import SpaceModify from './SpaceModify.vue'
import { UserRole } from '@/management/utils/types/workSpace'
const showSpaceModify = ref(false)
const modifyType = ref('edit')
const store = useStore()
const fields = ['name', 'surveyTotal', 'memberTotal', 'owner', 'createDate']
const fieldList = computed(() => {
return map(fields, (f) => {
return get(spaceListConfig, f, null)
})
})
const dataList = computed(() => {
return store.state.list.teamSpaceList
})
const isAdmin = (id: string) => {
return (
store.state.list.teamSpaceList.find((item: any) => item._id === id)?.currentUserRole ===
UserRole.Admin
)
}
const onRowClick = () => {
console.log('onRowClick')
}
const handleModify = async (id: string) => {
await store.dispatch('list/getSpaceDetail', id)
modifyType.value = 'edit'
showSpaceModify.value = true
}
const handleDelete = (id: string) => {
ElMessageBox.confirm(
'删除团队后,团队内的问卷将同步被删除,请谨慎考虑!是否确认本次删除?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(async () => {
await store.dispatch('list/deleteSpace', id)
await store.dispatch('list/getSpaceList')
})
.catch(() => {})
}
const onCloseModify = () => {
showSpaceModify.value = false
store.dispatch('list/getSpaceList')
}
// const handleCurrentChange = (current) => {
// this.currentPage = current
// this.init()
// }
</script>
<style lang="scss" scoped>
.list-wrap {
padding: 20px;
background: #fff;
.list-table {
:deep(.el-table__header) {
.tableview-header .el-table__cell {
.cell {
height: 24px;
color: #4a4c5b;
font-size: 14px;
}
}
}
:deep(.tableview-row) {
.tableview-cell {
padding: 5px 0;
&.link {
cursor: pointer;
}
.cell .cell-span {
font-size: 14px;
}
}
}
.tool-root {
display: flex;
&:first-child {
margin-left: -10px;
}
.tool-root-btn-text {
font-weight: normal !important;
}
}
}
}
</style>

View File

@ -0,0 +1,157 @@
<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
:disabled="formDisabled"
>
<el-form-item label="团队名称" prop="name">
<el-input v-model="formModel.name" />
</el-form-item>
<el-form-item label="空间描述">
<el-input v-model="formModel.description" />
</el-form-item>
<el-form-item label="添加成员" prop="members">
<MemberSelect
:members="formModel.members"
@select="handleMemberSelect"
@change="handleMembersChange"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="onClose" v-if="!formDisabled">取消</el-button>
<el-button type="primary" class="save-btn" @click="onConfirm" v-if="!formDisabled">
确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref, shallowRef, onMounted } from 'vue'
import { useStore } from 'vuex'
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 MemberSelect from './MemberSelect.vue'
import { type IMember, type IWorkspace, UserRole } from '@/management/utils/types/workSpace'
const store = useStore()
const emit = defineEmits(['on-close-codify', 'onFocus', 'change', 'blur'])
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<IWorkspace>({
name: '',
description: '',
members: [] as IMember[]
})
const rules = {
name: [{ required: true, message: '请输入团队名称', trigger: 'blur' }],
members: [
{
trigger: 'change',
validator: (rule: any, value: IMember[], callback: Function) => {
if (props.type === QOP_MAP.EDIT) {
if (value.filter((item: IMember) => item.role === UserRole.Admin).length === 0) {
callback('请至少设置一名空间管理员')
}
}
callback()
}
}
]
}
const spaceDetail = computed(() => {
return store.state.list.spaceDetail
})
const formDisabled = computed(() => {
return spaceDetail.value?._id
? store.state.list.teamSpaceList.find((item: any) => item._id === spaceDetail.value._id)
.currentUserRole !== UserRole.Admin
: false
})
onMounted(() => {
if (props.type === QOP_MAP.EDIT) {
formModel.value = _pick(spaceDetail.value, ['_id', 'name', 'description', 'members'])
}
})
const onClose = () => {
formModel.value = {
name: '',
description: '',
members: [] as IMember[]
}
//
store.commit('list/setSpaceDetail', 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', 'update')
} catch (err) {
ElMessage.error('createSpace status err' + err)
}
} else {
try {
await handleUpdate()
emit('on-close-codify', 'update')
} catch (err) {
ElMessage.error('createSpace status err' + err)
}
}
} else {
return false
}
})
}
const handleMemberSelect = (val: string, label: string) => {
formModel.value.members.push({ userId: val, username: label, role: UserRole.Member })
}
const handleMembersChange = (val: IMember[]) => {
formModel.value.members = val
}
const handleUpdate = async () => {
await store.dispatch('list/updateSpace', formModel.value)
}
const handleAdd = async () => {
await store.dispatch('list/addSpace', formModel.value)
}
</script>
<style lang="scss" rel="lang/scss" scoped>
.base-form-root {
padding: 20px;
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div :class="['list-state', 'list-state-' + value.curStatus.status]">
<div :class="['list-state', 'list-state-' + value.curStatus?.status]">
<span class="list-state-badge" />
<span>{{ statusMaps[value.curStatus.status] }}</span>
<span>{{ statusMaps[value.curStatus?.status] }}</span>
</div>
</template>

View File

@ -9,59 +9,38 @@
</div>
</template>
<script>
<script setup>
import { ref, computed } from 'vue'
import StaticIcon from './StaticIcon.vue'
export default {
name: 'TextButton',
components: {
StaticIcon
},
data() {
return {
iconIndex: 0
}
},
props: {
const props = defineProps({
option: {
type: Object,
required: true
},
effectFun: {
type: Function
},
effectKey: {
type: String
},
icon: {
type: String
}
},
computed: {
toggleOptionIcons() {
return this.option.icons.slice(1)
},
iconsLength() {
return this.toggleOptionIcons.length
},
currentIconItem() {
let finalIconIndex = this.iconIndex % this.iconsLength
return this.toggleOptionIcons[finalIconIndex]
}
},
methods: {
onClick() {
this.iconIndex++
if (this.iconIndex >= this.iconsLength) {
this.iconIndex = 0
}
typeof this.effectFun === 'function' &&
this.effectFun(this.currentIconItem.effectValue, this.effectKey)
}
},
created() {
this.iconIndex = this.toggleOptionIcons.findIndex((iconItem) => iconItem.isDefaultValue)
})
const emit = defineEmits(['change'])
const toggleOptionIcons = computed(() => {
return props.option.icons.slice(1)
})
const iconIndex = ref(0)
iconIndex.value = toggleOptionIcons.value.findIndex((iconItem) => iconItem.isDefaultValue)
const iconsLength = computed(() => {
return toggleOptionIcons.value.length
})
const currentIconItem = computed(() => {
let finalIconIndex = iconIndex.value % iconsLength.value
return toggleOptionIcons.value[finalIconIndex]
})
const onClick = () => {
iconIndex.value++
if (iconIndex.value >= iconsLength.value) {
iconIndex.value = 0
}
emit('change', currentIconItem.value.effectValue)
}
</script>

View File

@ -12,33 +12,28 @@
</div>
</template>
<script>
export default {
name: 'TextSelect',
data() {
return {
selectValue: this.options.default
}
<script setup>
import { computed } from 'vue'
const props = defineProps({
value: {
type: String,
default: ''
},
props: {
options: {
type: Object,
required: true
},
effectFun: {
type: Function
},
effectKey: {
type: String
}
})
const emit = defineEmits('change')
const selectValue = computed({
get() {
return props.value
},
watch: {
selectValue(newSelect) {
const { effectFun } = this
typeof effectFun === 'function' && effectFun(newSelect, this.effectKey)
set(val) {
emit('change', val)
}
}
}
})
</script>
<style lang="scss" scoped>

View File

@ -1,8 +1,8 @@
<template>
<div class="tool-bar-root">
<template v-if="tools.length">
<div class="tool-bar-root" @click="handleClick">
<template v-if="iconTools.length">
<ToolModule
v-for="t in tools"
v-for="t in iconTools"
:key="t.key"
:type="type"
:value="t.key"
@ -10,61 +10,44 @@
:width="toolWidth"
@call="onCall"
/>
<MoreTool
v-if="moreTools.length"
:type="type"
:width="toolWidth"
:tools="moreTools"
@call="onCall"
></MoreTool>
</template>
</div>
</template>
<script>
import { QOP_MAP } from '@/management/utils/constant'
<script setup>
import { computed } from 'vue'
import { slice } from 'lodash-es'
import ToolModule from './ToolModule.vue'
import MoreTool from './MoreTool.vue'
export default {
name: 'ToolBar',
props: {
const props = defineProps({
data: Object,
type: String,
toolWidth: Number,
tools: Array
},
data() {
return {}
},
methods: {
onCall(val) {
switch (val.key) {
case QOP_MAP.EDIT:
this.$emit('on-modify', this.data, QOP_MAP.EDIT)
return
case QOP_MAP.COPY:
this.$emit('on-modify', this.data, QOP_MAP.COPY)
return
case 'analysis':
this.$router.push({
name: 'analysisPage',
params: {
id: this.data._id
}
})
return
case 'release':
this.$router.push({
name: 'publish',
params: {
id: this.data._id
}
})
return
case 'delete':
this.$emit('on-delete', this.data)
return
default:
return
}
}
},
components: {
ToolModule
}
})
const emit = defineEmits(['click'])
const limit = 4
const iconTools = computed(() => {
return slice(props.tools, 0, limit - 1)
})
const moreTools = computed(() => {
return slice(props.tools, limit - 1)
})
const onCall = (val) => {
emit('click', val.key, props.data)
}
const handleClick = (e) => {
//
e.preventDefault()
e.stopPropagation()
}
</script>

View File

@ -43,6 +43,7 @@ export default {
text-align: center;
line-height: 16px;
cursor: pointer;
// font-weight: 500;
}
}
</style>

View File

@ -5,6 +5,35 @@ export const type = {
register: '在线报名'
}
export const spaceListConfig = {
name: {
title: '空间名称',
key: 'name',
width: 300
},
surveyTotal: {
title: '问卷数',
key: 'surveyTotal',
width: 150,
tip: true
},
memberTotal: {
title: '成员数',
key: 'memberTotal',
width: 150
},
owner: {
title: '所有者',
key: 'owner',
width: 150
},
createDate: {
title: '创建时间',
key: 'createDate',
minWidth: 200
}
}
export const fieldConfig = {
type: {
title: '类型',

View File

@ -1,45 +1,159 @@
<template>
<div class="question-list-root">
<div class="login-status">
<div class="top-nav">
<div class="left">
<img class="logo-img" src="/imgs/Logo.webp" alt="logo" />
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal">
<el-menu-item index="1">问卷列表</el-menu-item>
</el-menu>
</div>
<div class="login-info">
您好{{ userInfo?.username }}
<img class="login-info-img" src="/imgs/avatar.webp" />
<span class="logout" @click="handleLogout">退出</span>
</div>
</div>
<div class="content-wrap">
<SliderBar :menus="spaceMenus" @select="handleSpaceSelect" />
<div class="list-content">
<div class="top">
<h2>问卷列表</h2>
<el-button class="create-btn" type="default" @click="onCreate">
<h2>{{ spaceType === SpaceType.Group ? '团队空间' : '问卷' }}列表</h2>
<div class="operation">
<el-button
class="btn space-btn"
type="default"
@click="onSpaceCreate"
v-if="spaceType == SpaceType.Group"
>
<i class="iconfont icon-chuangjian"></i>
<span>创建团队空间</span>
</el-button>
<el-button
class="btn create-btn"
type="default"
@click="onCreate"
v-if="spaceType !== SpaceType.Group"
>
<i class="iconfont icon-chuangjian"></i>
<span>创建问卷</span>
</el-button>
</div>
<BaseList />
</div>
<BaseList
:loading="loading"
:data="surveyList"
:total="surveyTotal"
@reflush="fetchSurveyList"
v-if="spaceType !== SpaceType.Group"
></BaseList>
<SpaceList v-if="spaceType === SpaceType.Group"></SpaceList>
</div>
</div>
<SpaceModify
v-if="showSpaceModify"
:type="modifyType"
:visible="showSpaceModify"
@on-close-codify="onCloseModify"
/>
</div>
</template>
<script>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
import BaseList from './components/BaseList.vue'
import { mapState, mapActions } from 'vuex'
export default {
components: { BaseList },
name: 'listPage',
computed: {
...mapState('user', ['userInfo'])
},
methods: {
...mapActions('user', ['logout']),
onCreate() {
this.$router.push('/create')
},
handleLogout() {
this.logout()
this.$router.replace({ name: 'login' })
import SpaceList from './components/SpaceList.vue'
import SliderBar from './components/SliderBar.vue'
import SpaceModify from './components/SpaceModify.vue'
import { SpaceType } from '@/management/utils/types/workSpace'
const store = useStore()
const router = useRouter()
const userInfo = computed(() => {
return store.state.user.userInfo
})
const loading = ref(false)
const surveyList = computed(() => {
return store.state.list.surveyList
})
const surveyTotal = computed(() => {
return store.state.list.surveyTotal
})
const activeIndex = ref('1')
const spaceMenus = computed(() => {
return store.state.list.spaceMenus
})
const workSpaceId = computed(() => {
return store.state.list.workSpaceId
})
const spaceType = computed(() => {
return store.state.list.spaceType
})
const handleSpaceSelect = (id: any) => {
if (id === SpaceType.Personal) {
//
if (store.state.list.spaceType === SpaceType.Personal) {
return
}
store.commit('list/changeSpaceType', SpaceType.Personal)
store.commit('list/changeWorkSpace', '')
} else if (id === SpaceType.Group) {
//
if (store.state.list.spaceType === SpaceType.Group) {
return
}
store.commit('list/changeSpaceType', SpaceType.Group)
store.commit('list/changeWorkSpace', '')
} else if (!Object.values(SpaceType).includes(id)) {
//
if (store.state.list.workSpaceId === id) {
return
}
store.commit('list/changeSpaceType', SpaceType.Teamwork)
store.commit('list/changeWorkSpace', id)
}
fetchSurveyList()
}
onMounted(() => {
fetchSpaceList()
fetchSurveyList()
})
const fetchSpaceList = () => {
store.dispatch('list/getSpaceList')
}
const fetchSurveyList = async (params?: any) => {
if (!params) {
params = {
pageSize: 10,
curPage: 1
}
}
if (workSpaceId.value) {
params.workspaceId = workSpaceId.value
}
loading.value = true
await store.dispatch('list/getSurveyList', params)
loading.value = false
}
const modifyType = ref('add')
const showSpaceModify = ref(false)
const onCloseModify = (type: string) => {
showSpaceModify.value = false
if (type === 'update') fetchSpaceList()
}
const onSpaceCreate = () => {
showSpaceModify.value = true
}
const onCreate = () => {
router.push('/create')
}
const handleLogout = () => {
store.dispatch('user/logout')
router.replace({ name: 'login' })
}
</script>
@ -47,7 +161,8 @@ export default {
.question-list-root {
height: 100%;
background-color: #f6f7f9;
.login-status {
.top-nav {
background: #fff;
color: #4a4c5b;
padding: 0 20px;
@ -55,10 +170,25 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.04);
.left {
display: flex;
align-items: center;
width: calc(100% - 200px);
.logo-img {
width: 90px;
height: fit-content;
padding-right: 20px;
}
.el-menu {
width: 100%;
height: 56px;
border: none !important;
:deep(.el-menu-item, .is-active) {
border: none !important;
}
}
}
.login-info {
display: flex;
align-items: center;
@ -78,28 +208,46 @@ export default {
color: #faa600;
}
}
.list-content {
.content-wrap {
position: relative;
height: calc(100% - 56px);
padding: 20px;
}
.list-content {
position: relative;
height: 100%;
width: 100%;
padding: 32px 32px 32px 232px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
overflow: scroll;
.top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
.operation {
flex: 0 1 auto;
display: flex;
}
h2 {
font-size: 18px;
}
.create-btn {
background: #4a4c5b;
}
.space-btn {
background: $primary-color;
}
.btn {
width: 132px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
background: #4a4c5b;
color: #fff;
.icon-chuangjian {

View File

@ -1,6 +1,9 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { useStore } from 'vuex'
import { SurveyPermissions } from '@/management/utils/types/workSpace'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
const routes: RouteRecordRaw[] = [
{
@ -19,7 +22,8 @@ const routes: RouteRecordRaw[] = [
{
path: '/survey/:id/edit',
meta: {
needLogin: true
needLogin: true,
premissions: [SurveyPermissions.SurveyManage]
},
name: 'QuestionEdit',
component: () => import('../pages/edit/index.vue'),
@ -89,7 +93,8 @@ const routes: RouteRecordRaw[] = [
path: '/survey/:id/analysis',
name: 'analysisPage',
meta: {
needLogin: true
needLogin: true,
premissions: [SurveyPermissions.DataManage]
},
component: () => import('../pages/analysis/AnalysisPage.vue')
},
@ -97,7 +102,8 @@ const routes: RouteRecordRaw[] = [
path: '/survey/:id/publish',
name: 'publish',
meta: {
needLogin: true
needLogin: true,
premissions: [SurveyPermissions.SurveyManage]
},
component: () => import('../pages/publish/PublishPage.vue')
},
@ -125,7 +131,7 @@ const router = createRouter({
routes
})
router.beforeEach((to, from, next) => {
router.beforeEach(async (to, from, next) => {
const store = useStore()
if (!store.state.user?.initialized) {
store?.dispatch('user/init')
@ -135,7 +141,24 @@ router.beforeEach((to, from, next) => {
}
if (to.meta.needLogin) {
if (store?.state?.user?.hasLogined) {
if (to.meta.premissions) {
const params = to.params
await store.dispatch('fetchCooperPermissions', params.id)
if (
(to.meta.premissions as []).some((permission) =>
store.state?.cooperPermissions?.includes(permission)
)
) {
next()
} else {
ElMessage.warning('您没有该问卷的相关协作权限')
next({
name: 'survey'
})
}
} else {
next()
}
} else {
next({
name: 'login',
@ -149,4 +172,17 @@ router.beforeEach((to, from, next) => {
}
})
// router.afterEach(async (to, from) => {
// const store = useStore()
// if (to.meta.premissions) {
// const params = to.params
// await store.dispatch('fetchCooperPermissions', params.id)
// if (!(to.meta.premissions as []).some((permission) => store.state?.cooperPermissions?.includes(permission))) {
// ElMessage.warning('您没有该问卷的相关协作权限')
// router.push({
// name: 'survey'
// })
// }
// }
// })
export default router

View File

@ -1,4 +1,6 @@
import { getBannerData } from '@/management/api/skin.js'
import { getCollaboratorPermissions } from '@/management/api/space.ts'
import { CODE_MAP } from '../api/base'
export default {
async getBannerData({ state, commit }) {
@ -9,5 +11,12 @@ export default {
if (res.code === 200) {
commit('setBannerList', res.data)
}
},
async fetchCooperPermissions({ commit }, id) {
const res = await getCollaboratorPermissions(id)
console.log(res.data)
if (res.code === CODE_MAP.SUCCESS) {
commit('setCooperPermissions', res.data.permissions)
}
}
}

View File

@ -1,7 +1,7 @@
import { createStore } from 'vuex'
import edit from './edit'
import user from './user'
import list from './list'
import actions from './actions'
import mutations from './mutations'
import state from './state'
@ -13,6 +13,7 @@ export default createStore({
actions,
modules: {
edit,
user
user,
list
}
})

View File

@ -0,0 +1,258 @@
import {
createSpace,
getSpaceList,
getSpaceDetail,
updateSpace,
deleteSpace
} from '@/management/api/space'
import { CODE_MAP } from '@/management/api/base'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import { getSurveyList as surveyList } from '@/management/api/survey'
import { set } from 'lodash-es'
import { SpaceType } from '@/management/utils/types/workSpace'
export default {
namespaced: true,
state: {
// 空间管理
spaceMenus: [
{
icon: 'icon-wodekongjian',
name: '我的空间',
id: SpaceType.Personal
},
{
icon: 'icon-tuanduikongjian',
name: '团队空间',
id: SpaceType.Group,
children: [
// {
// name: '小桔问卷调研团队',
// id: 'xxxx',
// }
]
}
],
spaceType: SpaceType.Personal,
workSpaceId: '',
spaceDetail: null,
teamSpaceList: [],
// 列表管理
surveyList: [],
surveyTotal: 0,
searchVal: '',
selectValueMap: {
surveyType: '',
'curStatus.status': ''
},
buttonValueMap: {
'curStatus.date': '',
createDate: -1
}
},
getters: {
listFliter(state) {
return [
{
comparator: '',
condition: [
{
field: 'title',
value: state.searchVal,
comparator: '$regex'
}
]
},
{
comparator: '',
condition: [
{
field: 'curStatus.status',
value: state.selectValueMap['curStatus.status']
}
]
},
{
comparator: '',
condition: [
{
field: 'surveyType',
value: state.selectValueMap.surveyType
}
]
}
]
},
listOrder(state) {
const { buttonValueMap } = state
return Object.entries(buttonValueMap)
.filter(([, effectValue]) => effectValue)
.reduce((prev, item) => {
const [effectKey, effectValue] = item
prev.push({ field: effectKey, value: effectValue })
return prev
}, [])
}
},
mutations: {
updateSpaceMenus(state, teamSpace) {
// 更新空间列表下的团队空间
set(state, 'spaceMenus[1].children', teamSpace)
},
changeSpaceType(state, spaceType) {
state.spaceType = spaceType
},
changeWorkSpace(state, workSpaceId) {
// 切换空间清除筛选条件
this.commit('list/reserSelectValueMap')
this.commit('list/reserButtonValueMap')
this.commit('list/setSearchVal', '')
state.workSpaceId = workSpaceId
},
setSpaceDetail(state, data) {
state.spaceDetail = data
},
setTeamSpaceList(state, data) {
state.teamSpaceList = data
},
setSurveyList(state, list) {
state.surveyList = list
},
setSurveyTotal(state, total) {
state.surveyTotal = total
},
setSearchVal(state, data) {
state.searchVal = data
},
reserSelectValueMap(state) {
state.selectValueMap = {
surveyType: '',
'curStatus.status': ''
}
},
changeSelectValueMap(state, { key, value }) {
state.selectValueMap[key] = value
},
reserButtonValueMap(state) {
state.buttonValueMap = {
'curStatus.date': '',
createDate: -1
}
},
changeButtonValueMap(state, { key, value }) {
state.buttonValueMap[key] = value
}
},
actions: {
async getSpaceList({ commit }) {
try {
const res = await getSpaceList()
if (res.code === CODE_MAP.SUCCESS) {
const { list } = res.data
const teamSpace = list.map((item) => {
return {
id: item._id,
name: item.name
}
})
commit('setTeamSpaceList', list)
commit('updateSpaceMenus', teamSpace)
} else {
ElMessage.error('getSpaceList' + res.errmsg)
}
} catch (err) {
ElMessage.error('getSpaceList' + err)
}
},
async addSpace({}, params) {
const res = await createSpace({
name: params.name,
description: params.description,
members: params.members
})
if (res.code === CODE_MAP.SUCCESS) {
ElMessage.success('添加成功')
} else {
ElMessage.error('createSpace code err' + res.errmsg)
}
},
async getSpaceDetail({ state, commit }, id) {
try {
const workspaceId = id || state.workSpaceId
const res = await getSpaceDetail(workspaceId)
if (res.code === CODE_MAP.SUCCESS) {
commit('setSpaceDetail', res.data)
} else {
ElMessage.error('getSpaceList' + res.errmsg)
}
} catch (err) {
ElMessage.error('getSpaceList' + err)
}
},
async updateSpace({}, params) {
const res = await updateSpace({
workspaceId: params._id,
name: params.name,
description: params.description,
members: params.members
})
if (res.code === CODE_MAP.SUCCESS) {
ElMessage.success('更新成功')
} else {
ElMessage.error(res.errmsg)
}
},
async deleteSpace({}, workspaceId) {
try {
const res = await deleteSpace(workspaceId)
if (res.code === CODE_MAP.SUCCESS) {
ElMessage.success('删除成功')
} else {
ElMessage.error(res.errmsg)
}
} catch (err) {
ElMessage.error(err)
}
},
async getSurveyList({ state, getters, commit }, payload) {
const filterString = JSON.stringify(
getters.listFliter.filter((item) => {
return item.condition[0].value
})
)
const orderString = JSON.stringify(getters.listOrder)
try {
let params = {
curPage: payload?.curPage || 1,
pageSize: payload?.pageSize || 10, // 默认一页10条
filter: filterString,
order: orderString,
workspaceId: state.workSpaceId
}
// if(payload?.order) {
// params.order = payload.order
// }
// if(payload.filter) {
// params.filter = payload.filter
// }
// if(payload?.workspaceId) {
// params.workspaceId = payload.workspaceId
// }
const res = await surveyList(params)
if (res.code === CODE_MAP.SUCCESS) {
commit('setSurveyList', res.data.data)
commit('setSurveyTotal', res.data.count)
} else {
ElMessage.error(res.errmsg)
}
} catch (error) {
ElMessage.error('getSurveyList status' + error)
}
}
}
}

View File

@ -1,5 +1,8 @@
export default {
setBannerList(state, data) {
state.bannerList = data
},
setCooperPermissions(state, data) {
state.cooperPermissions = data
}
}

View File

@ -1,3 +1,5 @@
import { SurveyPermissions } from '@/management/utils/types/workSpace'
export default {
bannerList: []
bannerList: [],
cooperPermissions: Object.values(SurveyPermissions)
}

View File

@ -1,9 +1,9 @@
@font-face {
font-family: 'iconfont'; /* Project id 4263849 */
src:
url('//at.alicdn.com/t/c/font_4263849_xndlbqcha3l.woff2?t=1711101684869') format('woff2'),
url('//at.alicdn.com/t/c/font_4263849_xndlbqcha3l.woff?t=1711101684869') format('woff'),
url('//at.alicdn.com/t/c/font_4263849_xndlbqcha3l.ttf?t=1711101684869') format('truetype');
url('//at.alicdn.com/t/c/font_4263849_xfsn9z31epc.woff2?t=1716556097756') format('woff2'),
url('//at.alicdn.com/t/c/font_4263849_xfsn9z31epc.woff?t=1716556097756') format('woff'),
url('//at.alicdn.com/t/c/font_4263849_xfsn9z31epc.ttf?t=1716556097756') format('truetype');
}
.iconfont {
@ -131,3 +131,9 @@
.icon-NPSpingfen::before {
content: '\e6e7';
}
.icon-wodekongjian::before {
content: '\e6ee';
}
.icon-tuanduikongjian::before {
content: '\e6ec';
}

View File

@ -1,8 +1,10 @@
// 问卷操作枚举
export const QOP_MAP = {
ADD: 'add',
COPY: 'copy',
EDIT: 'edit'
}
export const qAbleList = ['radio', 'checkbox', 'binary-choice', 'vote']
export const operatorOptions = [
{

View File

@ -0,0 +1,59 @@
export interface ListItem {
value: string
label: string
}
export interface MenuItem {
id: string
name: string
icon?: string
children?: MenuItem[]
}
export type IWorkspace = {
id?: string
name: string
description: string
members: IMember[]
}
export type IMember = {
userId: string
username: string
role: any
_id?: string
}
export enum SpaceType {
Personal = 'personal',
Group = 'group',
Teamwork = 'teamwork'
}
export enum UserRole {
Admin = 'admin',
Member = 'user'
}
// 定义角色标签映射对象
export const roleLabels: Record<UserRole, string> = {
[UserRole.Admin]: '管理员',
[UserRole.Member]: '成员'
}
export interface ICollaborator {
_id?: string
userId: string
username: string
permissions: Array<number>
}
export enum SurveyPermissions {
SurveyManage = 'SURVEY_CONF_MANAGE',
DataManage = 'SURVEY_RESPONSE_MANAGE',
CollaboratorManage = 'SURVEY_COOPERATION_MANAGE'
}
// 定义协作者权限标签映射对象
export const surveyPermissionsLabels: Record<SurveyPermissions, string> = {
[SurveyPermissions.SurveyManage]: '问卷管理',
[SurveyPermissions.DataManage]: '数据管理',
[SurveyPermissions.CollaboratorManage]: '协作管理'
}

View File

@ -45,7 +45,7 @@ export default defineComponent({
return (
<div class="header-video-warp">
<div class="video">
<video {...this.attributeVideo} >
<video {...this.attributeVideo}>
<source src={this.bannerConf?.bannerConfig?.videoLink} type="video/mp4" />
</video>
{readonly ? (

View File

@ -18,7 +18,6 @@ export default defineComponent({
},
emits: ['select'],
setup(props, { emit }) {
const logoImage = computed(() => {
return props.logoConf?.logoImage
})
@ -56,7 +55,7 @@ export default defineComponent({
<div class="logo-icon-warp" onClick={this.onSelect}>
<div class="question-logo">
{this.logoImage ? (
<img src={this.logoImage} style={{width: this.logoImageWidth}} />
<img src={this.logoImage} style={{ width: this.logoImageWidth }} />
) : (
this.noLogoRender()
)}

View File

@ -1,4 +1,4 @@
import { defineComponent, computed,shallowRef,defineAsyncComponent} from 'vue'
import { defineComponent, computed, shallowRef, defineAsyncComponent } from 'vue'
import '@/render/styles/variable.scss'
import './index.scss'
@ -20,7 +20,6 @@ export default defineComponent({
},
emits: ['select', 'change'],
setup(props, { emit }) {
const titleClass = computed(() => {
let classStr = ''
if (!props.readonly) {
@ -59,9 +58,7 @@ export default defineComponent({
const richEditorView = shallowRef(null)
if (!props.readonly) {
richEditorView.value = defineAsyncComponent(
() => import('@/common/Editor/RichEditor.vue')
)
richEditorView.value = defineAsyncComponent(() => import('@/common/Editor/RichEditor.vue'))
}
return {
@ -75,12 +72,9 @@ export default defineComponent({
}
},
render() {
const { readonly,mainTitle,onTitleInput,richEditorView} = this;
const { readonly, mainTitle, onTitleInput, richEditorView } = this
return (
<div
class={['main-title-warp', !readonly ? 'pd15' : '']}
onClick={this.handleClick}
>
<div class={['main-title-warp', !readonly ? 'pd15' : '']} onClick={this.handleClick}>
{this.isTitleHide ? (
<div class={this.titleClass}>
{!readonly ? (

View File

@ -43,10 +43,7 @@ export default defineComponent({
render() {
const { submitConf } = this.props
return (
<div
class={['submit-warp', 'preview-submit_wrapper']}
onClick={this.handleClick}
>
<div class={['submit-warp', 'preview-submit_wrapper']} onClick={this.handleClick}>
<button class="submit-btn" type="primary" onClick={this.submit}>
{submitConf.submitTitle}
</button>

View File

@ -9,7 +9,11 @@
"
>
</Component>
<LogoIcon v-if="!['successPage', 'indexPage'].includes(store.state.router)" :logo-conf="logoConf" :readonly="true" />
<LogoIcon
v-if="!['successPage', 'indexPage'].includes(store.state.router)"
:logo-conf="logoConf"
:readonly="true"
/>
</div>
</template>
<script setup lang="ts">

View File

@ -10,7 +10,8 @@ export const useShowInput = (questionKey) => {
if (curRange.isShowInput) {
const rangeKey = `${questionKey}_${key}`
othersValue[rangeKey] = formValues[rangeKey]
;(curRange.othersKey = rangeKey), (curRange.othersValue = formValues[rangeKey])
curRange.othersKey = rangeKey
curRange.othersValue = formValues[rangeKey]
if (!questionVal.toString().includes(key) && formValues[rangeKey]) {
// 如果分值被未被选中且对应的填写更多有值,则清空填写更多
const data = {