🧹 清理重复配置文件

- 删除根目录中重复的 NestJS 配置文件
- 删除 tsconfig.json, tsconfig.build.json, eslint.config.mjs, .prettierrc
- 保留 wwjcloud-nest/ 目录中的完整配置
- 避免配置冲突,确保项目结构清晰
This commit is contained in:
wanwu
2025-10-14 23:56:20 +08:00
parent 7a160dd04b
commit e7a1d6b4d6
3263 changed files with 356 additions and 112679 deletions

View File

@@ -0,0 +1,30 @@
<template>
<div>
<el-image v-for="(item,index) in props.data.handle_field_value" :src="img(item)" class="w-[70px] h-[70px]" :class="{ 'mr-[5px]' : (index + 1) < props.data.handle_field_value.length }" fit="contain" :preview-src-list="imgList" :zoom-rate="1.2" :max-scale="7"
:min-scale="0.2" :initial-index="index" :hide-on-click-modal="true" />
</div>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue'
import { img } from '@/utils/common'
import { t } from "@/lang";
const props = defineProps({
data: {
type: Object,
default: () => {
return {}
}
}
})
const imgList = computed(() => {
return props.data.handle_field_value.map((item: any) => {
return img(item)
})
})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,25 @@
<template>
<div class="form-render">{{ props.data.render_value }}</div>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue'
import { t } from "@/lang";
const props = defineProps({
data: {
type: Object,
default: () => {
return {}
}
}
})
</script>
<style lang="scss" scoped>
.form-render {
word-wrap: break-word; /* 长单词或长字符串自动换行 */
word-break: break-word; /* 强制断词换行 */
white-space: pre-wrap; /* 保持空格和换行 */
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]">
<el-form-item :label="t('地址格式')">
<el-radio-group v-model="diyStore.editComponent.addressFormat" class="!block">
<el-radio class="!block" label="province/city/district/address">{{ t('省/市/区/街道/详细地址') }}</el-radio>
<el-radio class="!block" label="province/city/district/street">{{ t('省/市/区/街道(镇)') }}</el-radio>
<el-radio class="!block" label="province/city/district">{{ t('省/市/区(县)') }}</el-radio>
<el-radio class="!block" label="province/city">{{ t('省/市') }}</el-radio>
<el-radio class="!block" label="province">{{ t('省') }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
<el-form label-width="100px" class="px-[10px]">
<el-form-item class="display-block">
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('隐私保护') }}</span>
<el-tooltip effect="light" placement="top">
<template #content>
<p>会自动将提交的个人信息做加密展示</p>
<p>适用于公开展示收集的数据且不暴露用户隐私</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<el-switch v-model="diyStore.editComponent.field.privacyProtection" :disabled ="diyStore.editComponent.addressFormat != 'province/city/district/address'" />
<div class="text-sm text-gray-400">{{ t('提交后自动隐藏地址,仅管理员可查看') }}</div>
</el-form-item>
</el-form>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref,watch } from 'vue'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
return res
}
watch(
() => diyStore.editComponent.addressFormat,
(newVal) => {
if (newVal !== 'province/city/district/address') {
diyStore.editComponent.field.privacyProtection = false
}
},
{ immediate: true }
)
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,194 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]">
<el-form-item :label="t('style')">
<el-radio-group v-model="diyStore.editComponent.style">
<el-radio label="style-1">{{ t('defaultSources') }}</el-radio>
<el-radio label="style-2">{{ t('listStyle') }}</el-radio>
<el-radio label="style-3">{{ t('dropDownStyle') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('option')">
<div ref="formCheckboxRef">
<div v-for="(option, index) in diyStore.editComponent.options" :key="option.id" class="option-item flex items-center mb-[15px]">
<el-input v-model="diyStore.editComponent.options[index].text" class="!w-[215px]" :placeholder="t('optionPlaceholder')" maxlength="30" clearable />
<span v-if="diyStore.editComponent.options.length > 1" @click="removeOption(index)" class="cursor-pointer ml-[5px] nc-iconfont nc-icon-shanchu-yuangaizhiV6xx"></span>
</div>
</div>
<span class="text-primary cursor-pointer mr-[10px]" @click="addOption">{{ t('addSingleOption') }}</span>
<el-popover :visible="visible" placement="bottom" :width="300">
<p class="mb-[5px]">{{ t('addMultipleOption') }}</p>
<p class="text-[#888] text-[12px] mb-[5px]">{{ t('addOptionTips') }}</p>
<el-input v-model.trim="optionsValue" type="textarea" clearable maxlength="200" show-word-limit />
<div class="mt-[10px] text-right">
<el-button size="small" text @click="visible = false">{{ t('cancel') }}</el-button>
<el-button size="small" type="primary" @click="batchAddOptions">{{ t('confirm') }}</el-button>
</div>
<template #reference>
<span class="text-primary cursor-pointer" @click="visible = true">{{ t('addMultipleOption') }}</span>
</template>
</el-popover>
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
<!-- <el-form label-width="100px" class="px-[10px]">-->
<!-- <el-form-item class="display-block">-->
<!-- <template #label>-->
<!-- <div class="flex items-center">-->
<!-- <span class="mr-[3px]">{{ t('隐私保护') }}</span>-->
<!-- <el-tooltip effect="light" placement="top">-->
<!-- <template #content>-->
<!-- <p>会自动将提交的个人信息做加密展示</p>-->
<!-- <p>适用于公开展示收集的数据且不暴露用户隐私</p>-->
<!-- </template>-->
<!-- <el-icon>-->
<!-- <QuestionFilled color="#999999" />-->
<!-- </el-icon>-->
<!-- </el-tooltip>-->
<!-- </div>-->
<!-- </template>-->
<!-- <el-switch v-model="diyStore.editComponent.field.privacyProtection" />-->
<!-- <div class="text-sm text-gray-400">{{ t('提交后自动隐藏内容,仅管理员可查看') }}</div>-->
<!-- </el-form-item>-->
<!-- </el-form>-->
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref, onMounted, nextTick } from 'vue'
import useDiyStore from '@/stores/modules/diy'
import Sortable from 'sortablejs'
import { range } from 'lodash-es'
import { ElMessage } from 'element-plus'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
let pass = true
for (let i = 0; i < diyStore.value[index].options.length; i++) {
if (!diyStore.value[index].options[i].text) {
res.code = false
res.message = t('optionPlaceholder')
pass = false
break
}
}
if (!pass) return res
const uniqueOptions = uniqueByKey(diyStore.value[index].options, 'text')
if (uniqueOptions.length != diyStore.value[index].options.length) {
res.code = false
res.message = t('errorTipsOne')
}
return res
}
diyStore.editComponent.options.forEach((item: any) => {
if (!item.id) item.id = diyStore.generateRandom()
})
const visible = ref(false)
const optionsValue = ref()
const addOption = () => {
diyStore.editComponent.options.push({
id: diyStore.generateRandom(),
text: '选项' + (diyStore.editComponent.options.length + 1)
})
}
const removeOption = (index: any) => {
diyStore.editComponent.options.splice(index, 1)
}
// 批量添加选项
const batchAddOptions = () => {
if (optionsValue.value.trim()) {
const newOptions = optionsValue.value.split(',').map((option: any) => {
return {
id: diyStore.generateRandom(),
text: option.trim()
}
}).filter((option: any) => option.text !== '')
// 去除重复的选项
const uniqueNewOptions = uniqueByKey(newOptions, 'text')
// 过滤掉已存在的选项
const filteredNewOptions = uniqueNewOptions.filter((newOption: any) =>
!diyStore.editComponent.options.some((existingOption: any) => existingOption.text === newOption.text)
)
// 如果有新的选项,添加到选项列表中
if (filteredNewOptions.length > 0) {
diyStore.editComponent.options.push(...filteredNewOptions)
} else {
ElMessage({
message: t('errorTipsTwo'),
type: 'error'
})
}
optionsValue.value = ''
visible.value = false
}
}
// 数组去重
const uniqueByKey = (arr: any, key: any) => {
const seen = new Set()
return arr.filter((item: any) => {
const serializedKey = JSON.stringify(item[key])
return seen.has(serializedKey) ? false : seen.add(serializedKey)
})
}
const formCheckboxRef = ref()
onMounted(() => {
nextTick(() => {
const sortable = Sortable.create(formCheckboxRef.value, {
group: 'option-item',
animation: 200,
onEnd: event => {
const temp = diyStore.editComponent.options[event.oldIndex!]
diyStore.editComponent.options.splice(event.oldIndex!, 1)
diyStore.editComponent.options.splice(event.newIndex!, 0, temp)
sortable.sort(
range(diyStore.editComponent.options.length).map(value => {
return value.toString()
})
)
}
})
})
})
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,210 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]">
<el-form-item :label="t('dataFormat')">
<el-radio-group v-model="diyStore.editComponent.dateFormat">
<div class="flex flex-col">
<el-radio label="YYYY年M月D日">{{ dateFormat.format1 }}</el-radio>
<el-radio label="YYYY-MM-DD">{{ dateFormat.format2 }}</el-radio>
<el-radio label="YYYY/MM/DD">{{ dateFormat.format3 }}</el-radio>
</div>
</el-radio-group>
</el-form-item>
</el-form>
<div class="edit-attr-item-wrap">
<h3 class="mb-[10px]">{{ t('startDate') }}</h3>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.start.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
<el-form-item :label="t('defaultValue')">
<el-switch v-model="diyStore.editComponent.start.defaultControl" />
</el-form-item>
<el-form-item v-if="diyStore.editComponent.start.defaultControl">
<el-radio-group v-model="diyStore.editComponent.start.dateWay">
<el-radio label="current">{{ t('currentDate') }}</el-radio>
<el-radio label="diy">{{ t('diyDate') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="diyStore.editComponent.start.defaultControl && diyStore.editComponent.start.dateWay == 'diy'">
<el-date-picker v-model="diyStore.editComponent.field.default.start.date" format="YYYY/MM/DD" value-format="YYYY-MM-DD" type="date" :placeholder="t('startDataPlaceholder')" @change="startDateChange" />
</el-form-item>
</el-form>
</div>
<div class="edit-attr-item-wrap">
<h3 class="mb-[10px]">{{ t('endDate') }}</h3>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.end.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
<el-form-item :label="t('defaultValue')">
<el-switch v-model="diyStore.editComponent.end.defaultControl" />
</el-form-item>
<el-form-item v-if="diyStore.editComponent.end.defaultControl">
<el-radio-group v-model="diyStore.editComponent.end.dateWay">
<el-radio label="current">{{ t('currentDate') }}</el-radio>
<el-radio label="diy">{{ t('diyDate') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="diyStore.editComponent.end.defaultControl && diyStore.editComponent.end.dateWay == 'diy'">
<el-date-picker :disabled-date="disabledEndDate"
v-model="diyStore.editComponent.field.default.end.date" format="YYYY/MM/DD"
value-format="YYYY-MM-DD" type="date" :placeholder="t('endDataPlaceholder')"
@change="endDateChange" />
</el-form-item>
</el-form>
</div>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<div class="edit-attr-item-wrap">
<h3 class="mb-[10px]">{{ t('textStyle') }}</h3>
<el-form label-width="80px" class="px-[10px]">
<el-form-item :label="t('textFontSize')">
<el-slider v-model="diyStore.editComponent.fontSize" show-input size="small" class="ml-[10px] diy-nav-slider" :min="12" :max="18" />
</el-form-item>
<el-form-item :label="t('textFontWeight')">
<el-radio-group v-model="diyStore.editComponent.fontWeight">
<el-radio :label="'normal'">{{ t('fontWeightNormal') }}</el-radio>
<el-radio :label="'bold'">{{ t('fontWeightBold') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('textColor')">
<el-color-picker v-model="diyStore.editComponent.textColor" />
</el-form-item>
</el-form>
</div>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref, reactive, onMounted } from 'vue'
import { timeTurnTimeStamp } from '@/utils/common'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
let starTime = diyStore.value[index].field.default.start.date
let endTime = diyStore.value[index].field.default.end.date
const today = new Date()
const hours = String(today.getHours()).padStart(2, '0')
const minutes = String(today.getMinutes()).padStart(2, '0')
if (diyStore.editComponent.start.dateWay == 'current') {
starTime = today.toISOString().split('T')[0]
}
if (diyStore.editComponent.end.dateWay == 'current') {
endTime = today.toISOString().split('T')[0]
}
if (diyStore.editComponent.start.defaultControl && starTime == '' && diyStore.editComponent.end.dateWay == 'diy') {
res.code = false
res.message = t('startDataTips')
return res
}
if (diyStore.editComponent.end.defaultControl && endTime == '' && diyStore.editComponent.end.dateWay == 'diy') {
res.code = false
res.message = t('endDataTips')
return res
}
if (diyStore.editComponent.start.defaultControl && diyStore.editComponent.end.defaultControl && timeTurnTimeStamp(starTime) > timeTurnTimeStamp(endTime)) {
res.code = false
res.message = t('startEndDataTips')
return res
}
return res
}
const dateFormat: any = reactive({
format1: '',
format2: '',
format3: ''
})
// 结束日期-禁止的日期
const disabledEndDate = (time: Date) => {
let cutoffDate = null
let bool = false
if (diyStore.editComponent.start && diyStore.editComponent.start.defaultControl) {
if (diyStore.editComponent.start.dateWay == 'diy') {
cutoffDate = new Date(diyStore.editComponent.field.default.start.date)
} else {
cutoffDate = new Date()
}
bool = time.getTime() < cutoffDate.getTime()
}
return bool
}
onMounted(() => {
const today = new Date()
const endDate = new Date()
endDate.setDate(endDate.getDate() + 7) // 设置日期为7天后的日期
if (diyStore.editComponent.field.default.start.timestamp) {
diyStore.editComponent.field.default.start.date = today.toISOString().split('T')[0]
diyStore.editComponent.field.default.start.timestamp = parseInt(today.getTime() / 1000)
}
if (diyStore.editComponent.field.default.end.timestamp) {
diyStore.editComponent.field.default.end.date = endDate.toISOString().split('T')[0]
diyStore.editComponent.field.default.end.timestamp = parseInt(endDate.getTime() / 1000)
}
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
const hours = String(today.getHours()).padStart(2, '0')
const minutes = String(today.getMinutes()).padStart(2, '0')
dateFormat.format1 = `${year}${month}${day}`
dateFormat.format2 = `${year}-${month}-${day}`
dateFormat.format3 = `${year}/${month}/${day}`
dateFormat.format4 = `${year}-${month}-${day} ${hours}:${minutes}`
})
// 开始日期选择器
const startDateChange = (date) => {
diyStore.editComponent.field.default.start.date = date
diyStore.editComponent.field.default.start.timestamp = timeTurnTimeStamp(date)
const endDate = new Date(date)
endDate.setDate(endDate.getDate() + 7)
diyStore.editComponent.field.default.end.date = endDate.toISOString().split('T')[0]
diyStore.editComponent.field.default.end.timestamp = parseInt(endDate.getTime() / 1000)
}
// 结束日期选择器
const endDateChange = (date) => {
diyStore.editComponent.field.default.end.date = date
diyStore.editComponent.field.default.end.timestamp = timeTurnTimeStamp(date)
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,101 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('dataFormat')">
<el-radio-group v-model="diyStore.editComponent.dateFormat" class="!block">
<el-radio class="!block" label="YYYY年M月D日">{{ dateFormat.format1 }}</el-radio>
<el-radio class="!block" label="YYYY-MM-DD">{{ dateFormat.format2 }}</el-radio>
<el-radio class="!block" label="YYYY/MM/DD">{{ dateFormat.format3 }}</el-radio>
<el-radio class="!block" label="YYYY-MM-DD HH:mm">{{ dateFormat.format4 }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
<el-form-item :label="t('defaultValue')">
<el-switch v-model="diyStore.editComponent.defaultControl" />
</el-form-item>
<el-form-item v-if="diyStore.editComponent.defaultControl">
<el-radio-group v-model="diyStore.editComponent.dateWay">
<el-radio label="current">{{ t('currentDate') }}</el-radio>
<el-radio label="diy">{{ t('diyDate') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="diyStore.editComponent.defaultControl && diyStore.editComponent.dateWay == 'diy'">
<el-date-picker v-if="diyStore.editComponent.dateFormat != 'YYYY-MM-DD HH:mm'" v-model="diyStore.editComponent.field.default.date" format="YYYY/MM/DD" value-format="YYYY-MM-DD" type="date" :placeholder="t('dataPlaceholder')" @change="dateChange" />
<el-date-picker v-else v-model="diyStore.editComponent.field.default.date" format="YYYY/MM/DD HH:mm" value-format="YYYY-MM-DD HH:mm" type="datetime" :placeholder="t('dataPlaceholder')" @change="dateChange" />
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref, reactive, onMounted } from 'vue'
import { timeTurnTimeStamp } from '@/utils/common'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
return res
}
const dateFormat: any = reactive({
format1: '',
format2: '',
format3: '',
format4: ''
})
onMounted(() => {
// 初始赋值当天日期
const today = new Date()
if (!diyStore.editComponent.field.default.date) {
diyStore.editComponent.field.default.date = today.toISOString().split('T')[0]
diyStore.editComponent.field.default.timestamp = today.getTime() / 1000
}
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
const hours = String(today.getHours()).padStart(2, '0')
const minutes = String(today.getMinutes()).padStart(2, '0')
dateFormat.format1 = `${year}${month}${day}`
dateFormat.format2 = `${year}-${month}-${day}`
dateFormat.format3 = `${year}/${month}/${day}`
dateFormat.format4 = `${year}-${month}-${day} ${hours}:${minutes}`
})
const dateChange = (date: any) => {
diyStore.editComponent.field.default.date = date
diyStore.editComponent.field.default.timestamp = timeTurnTimeStamp(date)
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,48 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref } from 'vue'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
// todo 只需要考虑该组件自身的验证
return res
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,43 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('限制上传大小')">
<el-input v-model.trim="diyStore.editComponent.limitUploadSize" clearable maxlength="15" />
/单位MB目前是Bit要转换*1024
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref } from 'vue'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,85 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
<el-form label-width="100px" class="px-[10px]">
<el-form-item>
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('preventDuplication') }}</span>
<el-tooltip effect="light" placement="top">
<template #content>
<p>{{ t('preventDuplicationTipsOne') }}</p>
<p>{{ t('preventDuplicationTipsTwo') }}</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<el-switch v-model="diyStore.editComponent.field.unique" />
</el-form-item>
<el-form-item class="display-block">
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('privacyProtection') }}</span>
<el-tooltip effect="light" placement="top">
<template #content>
<p>{{ t('privacyProtectionTipsOne') }}</p>
<p>{{ t('privacyProtectionTipsTwo') }}</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
<div class="text-sm text-gray-400">{{ t('privacyProtectionTipsThree') }}</div>
</el-form-item>
</el-form>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref } from 'vue'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
// todo 只需要考虑该组件自身的验证
return res
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,88 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('imageLimit')">
<el-input v-model.trim="diyStore.editComponent.limit" :placeholder="t('imageLimitPlaceholder')" clearable maxlength="2" />
</el-form-item>
<el-form-item :label="t('上传方式')">
<el-checkbox-group v-model="diyStore.editComponent.uploadMode" :min="1">
<el-checkbox label="拍照上传" value="take_pictures" />
<el-checkbox label="从相册选择" value="select_from_album" />
</el-checkbox-group>
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref } from 'vue'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
if (diyStore.value[index].limit == '') {
res.code = false
res.message = t('imageLimitPlaceholder')
return res
}
if (isNaN(diyStore.value[index].limit) || !regExp.number.test(diyStore.value[index].limit)) {
res.code = false
res.message = t('imageLimitErrorTips')
return res
}
if (diyStore.value[index].limit < 0) {
res.code = false
res.message = t('imageLimitErrorTipsTwo')
return res
}
if (diyStore.value[index].limit == 0) {
res.code = false
res.message = t('imageLimitErrorTipsThree')
return res
}
if (diyStore.value[index].limit > 9) {
res.code = false
res.message = t('imageLimitErrorTipsFour')
return res
}
return res
}
// 正则表达式
const regExp: any = {
required: /[\S]+/,
number: /^\d{0,10}$/,
digit: /^\d{0,10}(.?\d{0,2})$/,
special: /^\d{0,10}(.?\d{0,3})$/
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,99 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
<el-form-item>
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('defaultValue') }}</span>
<el-tooltip effect="light" :content="t('defaultValueTips')" placement="top">
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input v-model.trim="diyStore.editComponent.field.default" :placeholder="t('defaultValuePlaceholder')" clearable maxlength="18" show-word-limit />
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
<el-form label-width="100px" class="px-[10px]">
<el-form-item>
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('preventDuplication') }}</span>
<el-tooltip effect="light" placement="top">
<template #content>
<p>{{ t('preventDuplicationTipsOne') }}</p>
<p>{{ t('preventDuplicationTipsTwo') }}</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<el-switch v-model="diyStore.editComponent.field.unique" />
</el-form-item>
<!-- <el-form-item class="display-block">-->
<!-- <template #label>-->
<!-- <div class="flex items-center">-->
<!-- <span class="mr-[3px]">{{ t('隐私保护') }}</span>-->
<!-- <el-tooltip effect="light" placement="top">-->
<!-- <template #content>-->
<!-- <p>会自动将提交的个人信息做加密展示</p>-->
<!-- <p>适用于公开展示收集的数据且不暴露用户隐私</p>-->
<!-- </template>-->
<!-- <el-icon>-->
<!-- <QuestionFilled color="#999999" />-->
<!-- </el-icon>-->
<!-- </el-tooltip>-->
<!-- </div>-->
<!-- </template>-->
<!-- <el-switch v-model="diyStore.editComponent.field.privacyProtection" />-->
<!-- <div class="text-sm text-gray-400">{{ t('提交后自动隐藏文本,仅管理员可查看') }}</div>-->
<!-- </el-form-item>-->
</el-form>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref } from 'vue'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
// todo 只需要考虑该组件自身的验证
return res
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,71 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]">
<el-form-item :label="t('access')">
<el-radio-group v-model="diyStore.editComponent.mode">
<el-radio class="!mr-[20px]" label="authorized_wechat_location">{{ t('authorizeWeChatLocation') }}</el-radio>
<el-radio label="open_choose_location">{{ t('manuallySelectPositioning') }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
<el-form label-width="100px" class="px-[10px]">
<el-form-item class="display-block">
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('privacyProtection') }}</span>
<el-tooltip effect="light" placement="top">
<template #content>
<p>{{ t('privacyProtectionTipsOne') }}</p>
<p>{{ t('privacyProtectionTipsTwo') }}</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
<div class="text-sm text-gray-400">{{ t('privacyProtectionTipsFour') }}</div>
</el-form-item>
</el-form>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import useDiyStore from '@/stores/modules/diy'
import { ref } from 'vue'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
// todo 只需要考虑该组件自身的验证
return res
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,85 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
<el-form label-width="100px" class="px-[10px]">
<el-form-item>
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('preventDuplication') }}</span>
<el-tooltip effect="light" placement="top">
<template #content>
<p>{{ t('preventDuplicationTipsOne') }}</p>
<p>{{ t('preventDuplicationTipsTwo') }}</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<el-switch v-model="diyStore.editComponent.field.unique" />
</el-form-item>
<el-form-item class="display-block">
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('privacyProtection') }}</span>
<el-tooltip effect="light" placement="top">
<template #content>
<p>{{ t('privacyProtectionTipsOne') }}</p>
<p>{{ t('privacyProtectionTipsTwo') }}</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
<div class="text-sm text-gray-400">{{ t('privacyProtectionTipsFive') }}</div>
</el-form-item>
</el-form>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import useDiyStore from '@/stores/modules/diy'
import { ref } from 'vue'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
// todo 只需要考虑该组件自身的验证
return res
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,119 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
<el-form-item :label="t('unit')">
<el-input v-model.trim="diyStore.editComponent.unit" :placeholder="t('unitPlaceholder')" clearable maxlength="5" show-word-limit />
</el-form-item>
<el-form-item>
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('defaultValue') }}</span>
<el-tooltip effect="light" :content="t('defaultValueTips')" placement="top">
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input v-model.trim="diyStore.editComponent.field.default" :placeholder="t('defaultValuePlaceholder')" @keyup="filterDigit($event)" clearable maxlength="18" show-word-limit />
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
<el-form label-width="100px" class="px-[10px]">
<!-- <el-form-item>-->
<!-- <template #label>-->
<!-- <div class="flex items-center">-->
<!-- <span class="mr-[3px]">{{ t('内容防重复') }}</span>-->
<!-- <el-tooltip effect="light" placement="top">-->
<!-- <template #content>-->
<!-- <p>该组件填写的内容不能与已提交的数据重复</p>-->
<!-- <p>极端情况下可能存在延时导致限制失效</p>-->
<!-- </template>-->
<!-- <el-icon>-->
<!-- <QuestionFilled color="#999999" />-->
<!-- </el-icon>-->
<!-- </el-tooltip>-->
<!-- </div>-->
<!-- </template>-->
<!-- <el-switch v-model="diyStore.editComponent.field.unique" />-->
<!-- </el-form-item>-->
<!-- <el-form-item class="display-block">-->
<!-- <template #label>-->
<!-- <div class="flex items-center">-->
<!-- <span class="mr-[3px]">{{ t('隐私保护') }}</span>-->
<!-- <el-tooltip effect="light" placement="top">-->
<!-- <template #content>-->
<!-- <p>会自动将提交的个人信息做加密展示</p>-->
<!-- <p>适用于公开展示收集的数据且不暴露用户隐私</p>-->
<!-- </template>-->
<!-- <el-icon>-->
<!-- <QuestionFilled color="#999999" />-->
<!-- </el-icon>-->
<!-- </el-tooltip>-->
<!-- </div>-->
<!-- </template>-->
<!-- <el-switch v-model="diyStore.editComponent.field.privacyProtection" />-->
<!-- <div class="text-sm text-gray-400">{{ t('提交后自动隐藏数字,仅管理员可查看') }}</div>-->
<!-- </el-form-item>-->
</el-form>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref } from 'vue'
import useDiyStore from '@/stores/modules/diy'
import { filterDigit } from '@/utils/common'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
if (diyStore.value[index].field.default) {
if (isNaN(diyStore.value[index].field.default) || !regExp.digit.test(diyStore.value[index].field.default)) {
res.code = false
res.message = t('defaultErrorTips')
} else if (diyStore.value[index].field.default < 0) {
res.code = false
res.message = t('defaultMustZeroTips')
}
}
return res
}
// 正则表达式
const regExp: any = {
required: /[\S]+/,
number: /^\d{0,10}$/,
digit: /^\d{0,10}(.?\d{0,2})$/,
special: /^\d{0,10}(.?\d{0,3})$/
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,214 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]">
<el-form-item :label="t('style')">
<el-radio-group v-model="diyStore.editComponent.style">
<el-radio label="style-1">{{ t('defaultSources') }}</el-radio>
<el-radio label="style-2">{{ t('listStyle') }}</el-radio>
<el-radio label="style-3">{{ t('dropDownStyle') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('option')">
<div ref="formRadioRef">
<div v-for="(option, index) in diyStore.editComponent.options" :key="option.id" class="option-item flex items-center mb-[15px]">
<el-input v-model="diyStore.editComponent.options[index].text" class="!w-[215px]" :placeholder="t('optionPlaceholder')" clearable maxlength="30" />
<span v-if="diyStore.editComponent.options.length > 1" @click="removeOption(index)" class="cursor-pointer ml-[5px] nc-iconfont nc-icon-shanchu-yuangaizhiV6xx"></span>
</div>
</div>
<span class="text-primary cursor-pointer mr-[10px]" @click="addOption">{{ t('addSingleOption') }}</span>
<el-popover :visible="visible" placement="bottom" :width="300">
<p class="mb-[5px]">{{ t('addMultipleOption') }}</p>
<p class="text-[#888] text-[12px] mb-[5px]">{{ t('addOptionTips') }}</p>
<el-input v-model.trim="optionsValue" type="textarea" clearable maxlength="200" show-word-limit />
<div class="mt-[10px] text-right">
<el-button size="small" text @click="visible = false">{{ t('cancel') }}</el-button>
<el-button size="small" type="primary" @click="batchAddOptions">{{ t('confirm') }}</el-button>
</div>
<template #reference>
<span class="text-primary cursor-pointer" @click="visible = true">{{ t('addMultipleOption') }}</span>
</template>
</el-popover>
</el-form-item>
<!-- <el-form-item class="display-block">
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('逻辑规则') }}</span>
<el-tooltip effect="light" placement="top">
<template #content>
<p>支持选择某个选项后显示特定的组件</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<div>
<el-button plain>{{ t('添加字段显示规则') }}</el-button>
<span class="mr-[3px]">1条字段显示规则</span>
<span class="text-primary cursor-pointer" @click="">设置</span>
</div>
</el-form-item> -->
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
<!-- <el-form label-width="100px" class="px-[10px]">-->
<!-- <el-form-item class="display-block">-->
<!-- <template #label>-->
<!-- <div class="flex items-center">-->
<!-- <span class="mr-[3px]">{{ t('隐私保护') }}</span>-->
<!-- <el-tooltip effect="light" placement="top">-->
<!-- <template #content>-->
<!-- <p>会自动将提交的个人信息做加密展示</p>-->
<!-- <p>适用于公开展示收集的数据且不暴露用户隐私</p>-->
<!-- </template>-->
<!-- <el-icon>-->
<!-- <QuestionFilled color="#999999" />-->
<!-- </el-icon>-->
<!-- </el-tooltip>-->
<!-- </div>-->
<!-- </template>-->
<!-- <el-switch v-model="diyStore.editComponent.field.privacyProtection" />-->
<!-- <div class="text-sm text-gray-400">{{ t('提交后自动隐藏内容,仅管理员可查看') }}</div>-->
<!-- </el-form-item>-->
<!-- </el-form>-->
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref, onMounted, nextTick } from 'vue'
import useDiyStore from '@/stores/modules/diy'
import Sortable from 'sortablejs'
import { range } from 'lodash-es'
import { ElMessage } from 'element-plus'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
let pass = true
for (let i = 0; i < diyStore.value[index].options.length; i++) {
if (!diyStore.value[index].options[i].text) {
res.code = false
res.message = t('optionPlaceholder')
pass = false
break
}
}
if (!pass) return res
const uniqueOptions = uniqueByKey(diyStore.value[index].options, 'text')
if (uniqueOptions.length != diyStore.value[index].options.length) {
res.code = false
res.message = t('errorTipsOne')
}
return res
}
diyStore.editComponent.options.forEach((item: any) => {
if (!item.id) item.id = diyStore.generateRandom()
})
const visible = ref(false)
const optionsValue = ref()
const addOption = () => {
diyStore.editComponent.options.push({
id: diyStore.generateRandom(),
text: '选项' + (diyStore.editComponent.options.length + 1)
})
}
const removeOption = (index: any) => {
diyStore.editComponent.options.splice(index, 1)
}
const batchAddOptions = () => {
if (optionsValue.value.trim()) {
const newOptions = optionsValue.value.split(',').map((option: any) => {
return {
id: diyStore.generateRandom(),
text: option.trim()
}
}).filter((option: any) => option.text !== '')
// 去除重复的选项
const uniqueNewOptions = uniqueByKey(newOptions, 'text')
// 过滤掉已存在的选项
const filteredNewOptions = uniqueNewOptions.filter(newOption =>
!diyStore.editComponent.options.some(existingOption => existingOption.text === newOption.text)
)
// 如果有新的选项,添加到选项列表中
if (filteredNewOptions.length > 0) {
diyStore.editComponent.options.push(...filteredNewOptions)
} else {
ElMessage({
message: t('errorTipsTwo'),
type: 'warning'
})
}
optionsValue.value = ''
visible.value = false
}
}
// 数组去重
const uniqueByKey = (arr: any, key: any) => {
const seen = new Set()
return arr.filter((item: any) => {
const serializedKey = JSON.stringify(item[key])
return seen.has(serializedKey) ? false : seen.add(serializedKey)
})
}
const formRadioRef = ref()
onMounted(() => {
nextTick(() => {
const sortable = Sortable.create(formRadioRef.value, {
group: 'option-item',
animation: 200,
onEnd: event => {
const temp = diyStore.editComponent.options[event.oldIndex!]
diyStore.editComponent.options.splice(event.oldIndex!, 1)
diyStore.editComponent.options.splice(event.newIndex!, 0, temp)
sortable.sort(
range(diyStore.editComponent.options.length).map(value => {
return value.toString()
})
)
}
})
})
})
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,117 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<div class="edit-attr-item-wrap">
<el-form label-width="80px" class="px-[10px]">
<el-form-item :label="t('floatBtnButton')" class="display-block">
<el-radio-group v-model="diyStore.editComponent.btnPosition" @change="btnPositionChangeFn">
<el-radio label="follow_content">{{ t('followContent') }}</el-radio>
<el-radio label="hover_screen_bottom">{{ t('hoverScreenBottom') }}</el-radio>
</el-radio-group>
<div class="text-sm text-gray-400 mb-[5px] leading-[1.4]"
v-show="diyStore.editComponent.btnPosition == 'follow_content'">{{ t('btnTips') }}
</div>
<div class="text-sm text-gray-400 mb-[5px]"
v-show="diyStore.editComponent.btnPosition == 'hover_screen_bottom'">{{ t('btnTipsTwo') }}
</div>
<div class="text-sm text-gray-400 mb-[10px] leading-[1.4]">{{ t('btnTipsThree') }}</div>
</el-form-item>
</el-form>
</div>
<div class="edit-attr-item-wrap">
<h3 class="mb-[10px]">{{ t('submitBtn') }}</h3>
<el-form label-width="80px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('submitBtnName')">
<el-input v-model.trim="diyStore.editComponent.submitBtn.text" :placeholder="t('btnNamePlaceholder')" clearable maxlength="10" show-word-limit />
</el-form-item>
<el-form-item :label="t('textColor')">
<el-color-picker v-model="diyStore.editComponent.submitBtn.color" />
</el-form-item>
<el-form-item :label="t('subTextBgColor')">
<el-color-picker v-model="diyStore.editComponent.submitBtn.bgColor" />
</el-form-item>
</el-form>
</div>
<div class="edit-attr-item-wrap">
<h3 class="mb-[10px]">{{ t('resetBtn') }}</h3>
<el-form label-width="80px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('carouselSearchTabControl')">
<el-switch v-model="diyStore.editComponent.resetBtn.control" />
</el-form-item>
<el-form-item :label="t('submitBtnName')">
<el-input v-model.trim="diyStore.editComponent.resetBtn.text" :placeholder="t('btnNamePlaceholder')" clearable maxlength="10" show-word-limit />
</el-form-item>
<el-form-item :label="t('textColor')">
<el-color-picker v-model="diyStore.editComponent.resetBtn.color" />
</el-form-item>
<el-form-item :label="t('subTextBgColor')">
<el-color-picker v-model="diyStore.editComponent.resetBtn.bgColor" />
</el-form-item>
</el-form>
</div>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<div class="edit-attr-item-wrap">
<h3 class="mb-[10px]">{{ t('btnStyle') }}</h3>
<el-form label-width="100px" class="px-[10px]">
<el-form-item :label="t('topRounded')">
<el-slider v-model="diyStore.editComponent.topElementRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
</el-form-item>
<el-form-item :label="t('bottomRounded')">
<el-slider v-model="diyStore.editComponent.bottomElementRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
</el-form-item>
</el-form>
</div>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref } from 'vue'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 单选
const btnPositionChangeFn = (e) => {
if (e == 'hover_screen_bottom') {
diyStore.editComponent.margin.bottom = 0
diyStore.editComponent.margin.both = 0
diyStore.editComponent.margin.top = 0
}
}
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
if (diyStore.value[index].submitBtn.text == '') {
res.code = false
res.message = t('submitBtnNamePlaceholder')
return res
}
if (diyStore.value[index].resetBtn.text == '') {
res.code = false
res.message = t('resetBtnNamePlaceholder')
return res
}
return res
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,405 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('列设置')">
<div ref="imageBoxRef">
<div v-for="(item, index) in diyStore.editComponent.columnList" :key="item.id"
class="border-b-[1px] border-[#e0e0e0] py-1">
<div class="flex items-center justify-between">
<div class="flex">
<span :class="['iconfont', 'ml-[5px]', 'cursor-pointer', getIconClass(item.type)]"></span>
<el-input v-model="item.name" class="input-style" :input-style="{ boxShadow: 'none' }"
:placeholder="t('请输入列名')" />
</div>
<div class="flex">
<span v-if="diyStore.editComponent.columnList.length > 1" @click="removeOption(index)"
class="cursor-pointer ml-[5px] nc-iconfont nc-icon-shanchu-yuangaizhiV6xx"></span>
<span class="cursor-pointer ml-[5px] nc-iconfont nc-icon-xiaV6xx"></span>
</div>
</div>
<div v-if="item.type == 'radio'" class="flex">
<div class="text-[#999] mr-3" >{{ item.options?.length || 0 }}个选项</div>
<span class="text-primary cursor-pointer mr-[10px]" @click="openRadioDialog(item, index)">{{ t('编辑') }}</span>
</div>
<div v-if="item.type == 'date'" class="flex">
<span class="text-primary cursor-pointer mr-[10px]" @click="openRadioDialog(item, index)">{{ t('设置日期格式') }}</span>
</div>
<div v-if="item.type == 'address'" class="flex">
<div class="text-[#999] mr-3">精确到详细地址</div>
<span class="text-primary cursor-pointer mr-[10px]" @click="openRadioDialog(item, index)">{{ t('设置') }}</span>
</div>
</div>
</div>
<el-popover placement="bottom" :width="50" trigger="hover">
<template #reference>
<span class="text-primary cursor-pointer mr-[10px]">{{ t('添加') }}</span>
</template>
<div v-for="(item, index) in columnTypeOptions" :key="index" @click="addOption(item)"
class="cursor-pointer hover:bg-[#d1e1ff] rounded text-center">
<div class="py-1 text-[var(--el-text-color-primary]">{{ item.label }}</div>
</div>
</el-popover>
</el-form-item>
<el-form-item :label="t('是否自增')">
<el-switch v-model="diyStore.editComponent.autoIncrementControl" />
</el-form-item>
<el-form-item :label="t('填写限制')" v-if="diyStore.editComponent.autoIncrementControl">
<div class="flex items-center">
<span>默认显示</span>
<el-input v-model="diyStore.editComponent.writeLimit.default" class="input-short" :placeholder="t('')" />
<span></span>
</div>
<div class="flex items-center my-1">
<span>最少填写</span>
<el-input v-model="diyStore.editComponent.writeLimit.min" class="input-short" :placeholder="t('')" />
<span></span>
</div>
<div class="flex items-center">
<span>最多填写</span>
<el-input v-model="diyStore.editComponent.writeLimit.max" class="input-short" :placeholder="t('')" />
<span></span>
</div>
</el-form-item>
<el-form-item :label="t('按钮名称')" v-if="diyStore.editComponent.autoIncrementControl">
<el-input v-model="diyStore.editComponent.btnText" :placeholder="t('请输入按钮名称')" />
</el-form-item>
</el-form>
<!-- 单选项 -->
<!-- <el-dialog v-model="radioDialogVisible" :title="t('设置单选项')" width="500">
<div v-if="activeColumnTemp.type == 'radio'">
<el-form label-width="80px" class="px-[10px]">
<el-form-item :label="t('选项名称')">
<el-input v-model="activeColumnTemp.name" :input-style="{ boxShadow: 'none' }" />
</el-form-item>
<el-form-item :label="t('设置选项')">
<div ref="radioBoxRef">
<div v-for="(opt, idx) in activeColumnTemp.options" :key="opt.id">
<div class="flex items-center justify-between mb-2">
<div class="flex-1">
<el-input v-model="opt.label" :input-style="{ boxShadow: 'none' }"
:placeholder="t('请输入')" />
</div>
<span v-if="activeColumnTemp.options.length > 1" @click="removeOptionItem(idx)"
class="cursor-pointer ml-[5px] nc-iconfont nc-icon-shanchu-yuangaizhiV6xx"></span>
<span class="cursor-pointer ml-[5px] nc-iconfont nc-icon-iconpaixu1"></span>
</div>
</div>
<span class="text-primary cursor-pointer mr-[10px]" @click="addOptionItem">{{ t('添加选项') }}</span>
<span class="text-primary cursor-pointer mr-[10px]" @click="addOtherOption">{{ t('添加其它项') }}</span>
<el-popover :visible="visible" placement="bottom" :width="300">
<p class="mb-[5px]">{{ t('addMultipleOption') }}</p>
<p class="text-[#888] text-[12px] mb-[5px]">{{ t('addOptionTips') }}</p>
<el-input v-model.trim="optionsValue" type="textarea" clearable maxlength="200"
show-word-limit />
<div class="mt-[10px] text-right">
<el-button size="small" text @click="visible = false">{{ t('cancel') }}</el-button>
<el-button size="small" type="primary" @click="batchAddOptions">{{t('confirm')}}</el-button>
</div>
<template #reference>
<span class="text-primary cursor-pointer"
@click="visible = true">{{ t('addMultipleOption') }}</span>
</template>
</el-popover>
</div>
</el-form-item>
</el-form>
</div>
<div v-else-if="activeColumnTemp.type == 'date'">
<el-form>
<el-form-item :label="t('dataFormat')">
<el-radio-group v-model="activeColumnTemp.dateFormat" class="!block">
<el-radio class="!block" label="YYYY年M月D日">{{ dateFormat.format1 }}</el-radio>
<el-radio class="!block" label="YYYY-MM-DD">{{ dateFormat.format2 }}</el-radio>
<el-radio class="!block" label="YYYY/MM/DD">{{ dateFormat.format3 }}</el-radio>
<el-radio class="!block" label="YYYY-MM-DD HH:mm">{{ dateFormat.format4 }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</div>
<div v-else-if="activeColumnTemp.type == 'address'">
<el-form>
<el-form-item :label="t('地址格式')">
<el-radio-group v-model="activeColumnTemp.addressFormat" class="!block">
<el-radio class="!block" label="province/city/district/address">{{ t('省/市/区/街道/详细地址') }}</el-radio>
<el-radio class="!block" label="province/city/district/street">{{ t('省/市/区/街道(镇)') }}</el-radio>
<el-radio class="!block" label="province/city/district">{{ t('省/市/区(县)') }}</el-radio>
<el-radio class="!block" label="province/city">{{ t('省/市') }}</el-radio>
<el-radio class="!block" label="province">{{ t('省') }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="radioDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleDialogConfirm">确定</el-button>
</div>
</template>
</el-dialog> -->
<div>
</div>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import Sortable from 'sortablejs'
import { ref, watch, onMounted, nextTick, reactive, computed } from 'vue'
import useDiyStore from '@/stores/modules/diy'
import { range } from 'lodash-es'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
// todo 只需要考虑该组件自身的验证
return res
}
// 类型选项数组
const columnTypeOptions = ref([
{ label: '单选项', value: 'radio' },
{ label: '文本', value: 'text' },
{ label: '数字', value: 'number' },
{ label: '手机号', value: 'mobile' },
{ label: '地址', value: 'address' },
{ label: '身份证', value: 'idcard' },
{ label: '性别', value: 'gender' },
{ label: '日期', value: 'date' }
])
const getIconClass = (type:any) => {
switch (type) {
case 'radio':
return 'icona-duihaopc30'
case 'text':
return 'icona-danhangwenben-1pc30'
case 'number':
return 'icona-shuzipc30-1'
case 'mobile':
return 'icona-shoujipc30'
case 'address':
return 'iconbiaotipc'
case 'idcard':
return 'icona-shenfenzhengpc30'
case 'gender':
return 'el-icon-s-opportunity'
case 'date':
return 'icona-riqipc30'
default:
return ''
}
}
const imageBoxRef = ref()
const generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2, 5)
// 添加列方法
const addOption = (item) => {
const newColumn: any = {
id: generateId(),
name: item.label,
type: item.value, // 列类型
value: '' // 默认值(可选)
}
// 如果是单选项,初始化 options
if (item.value === 'radio') {
newColumn.options = [
{ id: generateId(), label: '选项1' },
{ id: generateId(), label: '选项2' }
]
}
// 如果是日期,初始化 dateFormat
if (item.value === 'date') {
newColumn.dateFormat = 'YYYY年M月D日' // 默认日期格式
}
// 如果是地址,初始化 addressFormat
if (item.value === 'address') {
newColumn.addressFormat = 'province/city/district/address' // 默认日期格式
}
diyStore.editComponent.columnList.push(newColumn)
}
const removeOption = (index: number) => {
diyStore.editComponent.columnList.splice(index, 1)
}
onMounted(() => {
// nextTick(() => {
// if (diyStore.editComponent.columnList.length < 2) return;
// const sortable = Sortable.create(imageBoxRef.value, {
// group: 'item-wrap',
// animation: 200,
// onEnd: event => {
// const temp = diyStore.editComponent.columnList[event.oldIndex!]
// diyStore.editComponent.columnList.splice(event.oldIndex!, 1)
// diyStore.editComponent.columnList.splice(event.newIndex!, 0, temp)
// sortable.sort(
// range(diyStore.editComponent.columnList.length).map(value => {
// return value.toString()
// })
// )
// }
// })
// })
console.log(diyStore.editComponent.columnList)
})
const activeColumn = ref<any>({}) // 真正数据(原始数据,不动它)
const activeColumnTemp = ref<any>({}) // 弹窗编辑临时副本
const activeRadioIndex = ref(0) // 当前编辑列的下标
const radioDialogVisible = ref(false)
const radioBoxRef = ref()
const optionsValue = ref('')
const visible = ref(false)
const dateFormat: any = reactive({
format1: '',
format2: '',
format3: '',
format4: ''
})
const openRadioDialog = (item, index) => {
activeRadioIndex.value = index // 记录当前列的下标,方便确定时更新
activeColumn.value = item
activeColumnTemp.value = JSON.parse(JSON.stringify(item)) // 深拷贝,避免联动
if (item.type == 'radio') {
if (!activeColumnTemp.value.options) activeColumnTemp.value.options = []
radioDialogVisible.value = true
// nextTick(() => initRadioSortable()) // 拖拽初始化
} else if (item.type == 'date') {
// 初始赋值当天日期
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
const hours = String(today.getHours()).padStart(2, '0')
const minutes = String(today.getMinutes()).padStart(2, '0')
dateFormat.format1 = `${year}${month}${day}`
dateFormat.format2 = `${year}-${month}-${day}`
dateFormat.format3 = `${year}/${month}/${day}`
dateFormat.format4 = `${year}-${month}-${day} ${hours}:${minutes}`
radioDialogVisible.value = true
} else if (item.type == 'address') {
radioDialogVisible.value = true
}
}
// 初始化拖拽
// const initRadioSortable = () => {
// Sortable.create(radioBoxRef.value, {
// group: 'radio-option-wrap',
// animation: 200,
// draggable: '.drag-radio-item',
// onEnd: event => {
// const options = activeColumnTemp.value.options // 注意!这里用 temp 的
// const temp = options[event.oldIndex!]
// options.splice(event.oldIndex!, 1)
// options.splice(event.newIndex!, 0, temp)
// }
// })
// }
const handleDialogConfirm = () => {
console.log(activeColumnTemp.value)
diyStore.editComponent.columnList[activeRadioIndex.value] = JSON.parse(JSON.stringify(activeColumnTemp.value)) // 同步副本到原数据
radioDialogVisible.value = false // 关闭弹窗
}
const addOptionItem = () => {
const newOption = { id: generateId(), label: '选项' + (activeColumnTemp.value.options.length + 1) }
activeColumnTemp.value.options.push(newOption)
}
const addOtherOption = () => {
const newOption = { id: generateId(), label: '其他' }
activeColumnTemp.value.options.push(newOption)
}
const removeOptionItem = (index: number) => {
activeColumnTemp.value.options.splice(index, 1)
}
// 数组去重
const uniqueByKey = (arr: any, key: any) => {
const seen = new Set()
return arr.filter((item: any) => {
const serializedKey = JSON.stringify(item[key])
return seen.has(serializedKey) ? false : seen.add(serializedKey)
})
}
// 批量添加
const batchAddOptions = () => {
if (optionsValue.value.trim()) {
const newOptions = optionsValue.value.split(',').map((option: any) => {
return {
id: diyStore.generateRandom(),
label: option.trim()
}
}).filter((option: any) => option.label !== '')
// 去除重复的选项
const uniqueNewOptions = uniqueByKey(newOptions, 'label')
// 过滤掉已存在的选项
const filteredNewOptions = uniqueNewOptions.filter(newOption =>
!activeColumnTemp.value.options.some(existingOption => existingOption.label === newOption.label)
)
// 如果有新的选项,添加到选项列表中
if (filteredNewOptions.length > 0) {
activeColumnTemp.value.options.push(...filteredNewOptions)
} else {
ElMessage({
message: t('errorTipsTwo'),
type: 'warning'
})
}
optionsValue.value = ''
visible.value = false
}
}
defineExpose({})
</script>
<style lang="scss" scoped>
:deep(.input-style .el-input__wrapper) {
box-shadow: none !important;
}
.input-short{
width: 80px;
margin: 0 10px;
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
<el-form-item>
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('defaultValue') }}</span>
<el-tooltip effect="light" :content="t('defaultValueTips')" placement="top">
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input v-model.trim="diyStore.editComponent.field.default"
:placeholder="t('defaultValuePlaceholder')" clearable maxlength="18" show-word-limit />
</el-form-item>
<el-form-item :label="t('rowCount')">
<el-input v-model.trim="diyStore.editComponent.rowCount" :placeholder="t('rowCountPlaceholder')" clearable maxlength="2" show-word-limit />
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
<!--<el-form label-width="100px" class="px-[10px]">
<el-form-item class="display-block">
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('隐私保护') }}</span>
<el-tooltip effect="light" placement="top">
<template #content>
<p>会自动将提交的个人信息做加密展示</p>
<p>适用于公开展示收集的数据且不暴露用户隐私</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
<div class="text-sm text-gray-400">{{ t('提交后自动隐藏文本,仅管理员可查看') }}</div>
</el-form-item>
</el-form>-->
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref } from 'vue'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
// todo 只需要考虑该组件自身的验证
return res
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,201 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<div class="edit-attr-item-wrap">
<h3 class="mb-[10px]">{{ t('startTime') }}</h3>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.start.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
<el-form-item :label="t('defaultValue')">
<el-switch v-model="diyStore.editComponent.start.defaultControl" />
</el-form-item>
<el-form-item v-if="diyStore.editComponent.start.defaultControl">
<el-radio-group v-model="diyStore.editComponent.start.timeWay">
<el-radio label="current">{{ t('currentTime') }}</el-radio>
<el-radio label="diy">{{ t('diyTime') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="diyStore.editComponent.start.defaultControl && diyStore.editComponent.start.timeWay == 'diy'">
<el-time-picker v-model="diyStore.editComponent.field.default.start.date" :placeholder="t('startTimePlaceholder')" format="HH:mm" value-format="HH:mm" @change="startTimePickerChange" />
</el-form-item>
</el-form>
</div>
<div class="edit-attr-item-wrap">
<h3 class="mb-[10px]">{{ t('endTime') }}</h3>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.end.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
<el-form-item :label="t('defaultValue')">
<el-switch v-model="diyStore.editComponent.end.defaultControl" />
</el-form-item>
<el-form-item v-if="diyStore.editComponent.end.defaultControl">
<el-radio-group v-model="diyStore.editComponent.end.timeWay">
<el-radio label="current">{{ t('currentTime') }}</el-radio>
<el-radio label="diy">{{ t('diyTime') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="diyStore.editComponent.end.defaultControl && diyStore.editComponent.end.timeWay == 'diy'">
<el-time-picker :disabled-hours="disabledHours" :disabled-minutes="disabledMinutes" v-model="diyStore.editComponent.field.default.end.date" :placeholder="t('endTimePlaceholder')" format="HH:mm" value-format="HH:mm" />
</el-form-item>
</el-form>
</div>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<div class="edit-attr-item-wrap">
<h3 class="mb-[10px]">{{ t('textStyle') }}</h3>
<el-form label-width="80px" class="px-[10px]">
<el-form-item :label="t('textFontSize')">
<el-slider v-model="diyStore.editComponent.fontSize" show-input size="small" class="ml-[10px] diy-nav-slider" :min="12" :max="18" />
</el-form-item>
<el-form-item :label="t('textFontWeight')">
<el-radio-group v-model="diyStore.editComponent.fontWeight">
<el-radio :label="'normal'">{{ t('fontWeightNormal') }}</el-radio>
<el-radio :label="'bold'">{{ t('fontWeightBold') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('textColor')">
<el-color-picker v-model="diyStore.editComponent.textColor" />
</el-form-item>
</el-form>
</div>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref, onMounted } from 'vue'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
let starTime = diyStore.value[index].field.default.start.date
let endTime = diyStore.value[index].field.default.end.date
const today = new Date()
const hours = String(today.getHours()).padStart(2, '0')
const minutes = String(today.getMinutes()).padStart(2, '0')
if (diyStore.editComponent.start.timeWay == 'current') {
starTime = `${hours}:${minutes}`
}
if (diyStore.editComponent.end.timeWay == 'current') {
endTime = `${hours}:${minutes}`
}
if (diyStore.editComponent.start.defaultControl && starTime == '') {
res.code = false
res.message = t('startTimeTips')
return res
}
if (diyStore.editComponent.end.defaultControl && endTime == '') {
res.code = false
res.message = t('endTimeTips')
return res
}
if (diyStore.editComponent.start.defaultControl && diyStore.editComponent.end.defaultControl && timeInvertSecond(starTime) > timeInvertSecond(endTime)) {
res.code = false
res.message = t('startEndTimeTips')
return res
}
return res
}
onMounted(() => {
const today = new Date()
const hours = String(today.getHours()).padStart(2, '0')
const minutes = String(today.getMinutes()).padStart(2, '0')
if (!diyStore.editComponent.field.default.start.date) {
diyStore.editComponent.field.default.start.date = `${hours}:${minutes}`
diyStore.editComponent.field.default.start.timestamp = timeInvertSecond(`${hours}:${minutes}`)
}
if (!diyStore.editComponent.field.default.end.date) {
const endDate = new Date()
endDate.setHours(today.getHours(), today.getMinutes() + 10, 0, 0) // 在当前时间基础上加 10 分钟
const endHours = String(endDate.getHours()).padStart(2, '0')
const endMinutes = String(endDate.getMinutes()).padStart(2, '0')
diyStore.editComponent.field.default.end.date = `${endHours}:${endMinutes}`
diyStore.editComponent.field.default.end.timestamp = timeInvertSecond(`${endHours}:${endMinutes}`)
}
})
// 开始时间选择器
const startTimePickerChange = (e) => {
diyStore.editComponent.field.default.start.timestamp = timeInvertSecond(e)
const startTimeArr = e.split(':')
const date = new Date()
date.setHours(parseInt(startTimeArr[0]), parseInt(startTimeArr[1]), 0, 0)
date.setMinutes(date.getMinutes() + 10)
const updatedEndTime = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
diyStore.editComponent.field.default.end.date = updatedEndTime
diyStore.editComponent.field.default.end.timestamp = timeInvertSecond(updatedEndTime)
}
// 结束时间选择器
const endTimePickerChange = (e) => {
diyStore.editComponent.field.default.end.timestamp = timeInvertSecond(e)
}
const disabledHours = () => {
const timeArr = diyStore.editComponent.field.default.start.date.split(':')
return makeRange(0, timeArr[0])
}
const disabledMinutes = (hour: number) => {
const timeArr = diyStore.editComponent.field.default.start.date.split(':')
return makeRange(0, timeArr[1])
}
const makeRange = (start: number, end: number) => {
const result: number[] = []
for (let i = start; i < end; i++) {
result.push(i)
}
return result
}
const timeInvertSecond = (time: any) => {
const arr = time.split(':')
let num = 0
if (arr[0]) {
num += arr[0] * 60 * 60
}
if (arr[1]) {
num += arr[1] * 60
}
if (arr[2]) {
num += arr[2]
}
return num
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,89 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
<el-form-item :label="t('defaultValue')">
<el-switch v-model="diyStore.editComponent.defaultControl" @change="changeDateDefaultControl" />
</el-form-item>
<el-form-item v-if="diyStore.editComponent.defaultControl">
<el-radio-group v-model="diyStore.editComponent.timeWay">
<el-radio label="current">{{ t('currentTime') }}</el-radio>
<el-radio label="diy">{{ t('diyTime') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="diyStore.editComponent.defaultControl && diyStore.editComponent.timeWay == 'diy'">
<el-time-picker v-model="diyStore.editComponent.field.default" :placeholder="t('timePlaceholder')" format="HH:mm" value-format="HH:mm" />
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref, watch, onMounted } from 'vue'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
return res
}
onMounted(() => {
// 初始赋值当天时间
if (!diyStore.editComponent.field.default) {
const today = new Date()
const hours = String(today.getHours()).padStart(2, '0')
const minutes = String(today.getMinutes()).padStart(2, '0')
diyStore.editComponent.field.default = `${hours}:${minutes}`
}
})
const changeDateDefaultControl = (val: any) => {
if (val) {
const today = new Date()
const hours = String(today.getHours()).padStart(2, '0')
const minutes = String(today.getMinutes()).padStart(2, '0')
diyStore.editComponent.field.default = `${hours}:${minutes}`
}
}
watch(
() => diyStore.editComponent.timeWay,
(newVal) => {
const today = new Date()
const hours = String(today.getHours()).padStart(2, '0')
const minutes = String(today.getMinutes()).padStart(2, '0')
diyStore.editComponent.field.default = `${hours}:${minutes}`
}
)
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,54 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]">
<el-form-item>
<template #label>
<div class="flex items-center">
<span class="mr-[3px]">{{ t('上传方式') }}</span>
<el-tooltip effect="light" :content="t('拍摄时长限制1分钟从相册上传不限制时长。')" placement="top">
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</template>
<el-radio-group v-model="diyStore.editComponent.uploadMode">
<el-radio label="shoot_and_album">{{ t('拍摄和相册') }}</el-radio>
<el-radio label="shoot_only">{{ t('只允许拍摄') }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref } from 'vue'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,48 @@
<template>
<!-- 内容 -->
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
<!-- 表单组件 字段内容设置 -->
<slot name="field"></slot>
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
<el-form-item :label="t('formPlaceholder')">
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
</el-form-item>
</el-form>
<!-- 表单组件 其他设置 -->
<slot name="other"></slot>
</div>
<!-- 样式 -->
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
<!-- 表单组件 字段样式 -->
<slot name="style-field"></slot>
<!-- 组件样式 -->
<slot name="style"></slot>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref } from 'vue'
import useDiyStore from '@/stores/modules/diy'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
// 组件验证
diyStore.editComponent.verify = (index: number) => {
const res = { code: true, message: '' }
// todo 只需要考虑该组件自身的验证
return res
}
defineExpose({})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,177 @@
<template>
<div>
<el-form :inline="true" :model="tableData.searchParam" ref="searchFormRef">
<el-form-item :label="t('formSelectContentTitle')" prop="title" class="form-item-wrap">
<el-input v-model.trim="tableData.searchParam.title" :placeholder="t('formSelectContentTitlePlaceholder')" />
</el-form-item>
<el-form-item :label="t('formSelectContentTypeName')" prop="type" class="form-item-wrap">
<el-select v-model="tableData.searchParam.type" :placeholder="t('formSelectContentTypeNamePlaceholder')">
<el-option :label="t('formSelectContentTypeAll')" value="" />
<el-option v-for="(item, key) in formType" :label="item.title" :value="key" :key="key" />
</el-select>
</el-form-item>
<el-form-item class="form-item-wrap">
<el-button type="primary" @click="loadList()">{{ t('search') }}</el-button>
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
</el-form-item>
</el-form>
<el-table :data="tableData.data" size="large" ref="tableRef" v-loading="tableData.loading">
<template #empty>
<span>{{ !tableData.loading ? t('emptyData') : '' }}</span>
</template>
<el-table-column min-width="7%">
<template #default="{ row }">
<el-checkbox v-model="row.checked" @change="handleCheckChange($event,row)" />
</template>
</el-table-column>
<el-table-column prop="page_title" :label="t('formSelectContentTitle')" min-width="65%" />
<el-table-column prop="type_name" :label="t('formSelectContentTypeName')" min-width="25%" />
</el-table>
<div class="mt-[16px] flex justify-end">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit"
layout="total, sizes, prev, pager, next, jumper" :total="tableData.total"
@size-change="loadList()" @current-change="loadList" />
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, nextTick } from 'vue'
import { t } from '@/lang'
import { getFormType, getDiyFormSelectPageList } from '@/app/api/diy_form'
import { FormInstance, ElMessage } from 'element-plus'
const prop = defineProps({
formId: {
type: [Number, String],
default: 0
}
})
const formType: any = reactive({}) // 表单类型
const searchFormRef = ref<FormInstance>()
const tableRef = ref()
const tableData: any = reactive({
page: 1,
limit: 10,
total: 0,
loading: true,
data: [],
searchParam: {
title: '',
type: '',
verify_form_ids: []
}
})
// 已选万能表单
const selectData: any = reactive({
form_id: prop.formId
})
// 获取自定义表单列表
const loadList = (page: number = 1) => {
tableData.loading = true
tableData.page = page
if (selectData.form_id) {
tableData.searchParam.verify_form_ids = [selectData.form_id]
}
getDiyFormSelectPageList({
page: tableData.page,
limit: tableData.limit,
...tableData.searchParam
}).then(res => {
tableData.loading = false
tableData.data = res.data.data
tableData.data.forEach((item: any) => {
item.checked = item.form_id == selectData.form_id
})
tableData.total = res.data.total
setGoodsSelected()
}).catch(() => {
tableData.loading = false
})
}
// 获取万能表单类型
const loadFormType = (addon = '') => {
getFormType({}).then(res => {
for (const key in formType) {
delete formType[key]
}
for (const key in res.data) {
formType[key] = res.data[key]
}
})
}
loadFormType()
loadList()
const handleCheckChange = (isSelect: any, row: any) => {
if (isSelect) {
selectData.form_id = row.form_id
} else {
selectData.form_id = 0 // 未选中,移除当前
}
setGoodsSelected()
}
// 表格设置选中状态
const setGoodsSelected = () => {
nextTick(() => {
for (let i = 0; i < tableData.data.length; i++) {
tableData.data[i].checked = false
if (selectData.form_id == tableData.data[i].form_id) {
tableData.data[i].checked = true
Object.assign(selectData, tableData.data[i])
}
}
})
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
loadList()
}
const getData = () => {
if (selectData.form_id == 0) {
ElMessage({
type: 'warning',
message: `${t('formSelectContentTips')}`
})
return
}
return {
name: 'DIY_FORM',
title: selectData.page_title,
url: `/app/pages/index/diy_form?form_id=${selectData.form_id}`,
action: '',
formId: selectData.form_id
}
}
defineExpose({
getData
})
</script>
<style lang="scss" scoped>
.form-item-wrap {
margin-right: 10px !important;
margin-bottom: 10px !important;
&.last-child {
margin-right: 0 !important;
}
}
</style>

View File

@@ -0,0 +1,435 @@
<template>
<div>
<el-dialog v-model="showDialog" :title="t('submitSuccess')" width="850px" :close-on-press-escape="true" :destroy-on-close="true" :close-on-click-modal="false">
<div class="flex flex-1 mt-[24px] mx-[24px] mb-0">
<div class="preview-wrap">
<div class="absolute z-1 left-0 top-0">
<img src="@/app/assets/images/diy_form/mobile_tabbar.png" class="w-[324px]" />
</div>
<div class="absolute z-1 left-0 bottom-0">
<img src="@/app/assets/images/diy_form/mobile_bottom.png" class="w-[324px]" />
</div>
<div class="page-wrap">
<div class="px-[13px] flex flex-col items-center flex-1">
<div class="flex items-center justify-center w-[48px] h-[48px] text-[40px] p-[4px] mt-[32px] mb-[16px] mx-auto rounded-[50%]">
<el-icon><SuccessFilled color="#20bf64" /></el-icon>
</div>
<div class="record-name">
<span class="text-[#1E1E1E] font-bold text-[24px]" v-if="formData.tips_type == 'default'">{{ t('writeSuccess') }}</span>
<span class="text-[#1E1E1E] font-bold text-[16px]" v-else-if="formData.tips_type == 'diy'">{{ formData.tips_text ? formData.tips_text : '填写成功' }}</span>
</div>
<div class="to-detail">
<div class="text-[14px] mt-[16px] py-[4px] px[8px] text-[#576b95]">{{ t('viewFillingDetails') }}</div>
</div>
</div>
<div class="relative pt-[8px] pb-[48px] h-[112px]">
<div v-if="formData.success_after_action.finish" class="!mt-[16px] rounded-[3px] mx-auto text-[15px] w-[100px] min-w-[160px] h-[32px] leading-[32px] text-center max-w-[274px] truncate bg-[#20bf64] text-[#ffffff]">
<div class="text-[15px]">{{ t('finish') }}</div>
</div>
<div v-if="formData.success_after_action.goback" class="!mt-[16px] rounded-[3px] mx-auto text-[15px] w-[100px] min-w-[160px] h-[32px] leading-[32px] text-center max-w-[274px] truncate bg-[#f2f2f2] text-[#353535]">
<div class="text-[14px]">{{ t('back') }}</div>
</div>
</div>
</div>
<!-- 核销凭证 todo 后续完善 -->
<!-- <div class="page-wrap verify-voucher-wrap" style="display:none;">-->
<!-- <div class="tips-wrap">感谢你的填写以下是你的核销凭证</div>-->
<!-- <div class="qrcode-wrap">-->
<!-- <div class="text-[14px] text-[#333]">请妥善保存你的核销凭证</div>-->
<!-- <div class="text-[20px] font-bold text-[#333] my-[10px]">现场出示凭证</div>-->
<!-- <el-image class="w-[180px]" :src="wapImage" />-->
<!-- <div class="text-primary mt-[10px]">保存凭证</div>-->
<!-- </div>-->
<!-- <div class="relative pt-[8px] pb-[48px] h-[112px]">-->
<!-- <div class="!mt-[16px] rounded-[3px] mx-auto text-[15px] w-[100px] min-w-[160px] h-[32px] leading-[32px] text-center max-w-[274px] truncate bg-[#20bf64] text-[#ffffff]">-->
<!-- <div class="text-[15px]">返回二维码</div>-->
<!-- </div>-->
<!-- <div class="!mt-[16px] rounded-[3px] mx-auto text-[15px] w-[100px] min-w-[160px] h-[32px] leading-[32px] text-center max-w-[274px] truncate bg-[#fff] text-[#353535]">-->
<!-- <div class="text-[14px]">完成</div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="text-[14px] mt-[16px] py-[4px] px[8px] text-[#576b95]">查看填写详情</div>-->
<!-- </div>-->
</div>
<div class="flex-1">
<div class="item-wrap">
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">{{ t('afterSubmission') }}</div>
<el-radio-group v-model="formData.submit_after_action" class="!block">
<el-radio label="text" class="!flex">
<span class="mr-[3px]">{{ t('displayTextMessages') }}</span>
<el-tooltip effect="light" placement="top">
<template #content>
<p>{{ t('displayTextMessagesTips') }}</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</el-radio>
<!-- todo 后续完善 -->
<!-- <el-radio label="voucher" class="!flex">-->
<!-- <span class="mr-[3px]">{{ t('获取核销凭证') }}</span>-->
<!-- <el-tooltip effect="light" placement="top">-->
<!-- <template #content>-->
<!-- <p>{{ t('提交后页面会将提交的表单记录内容生成二维码并展示,可选择设置两种不同的二维码内容。适合核销、数据录入等场景。') }}</p>-->
<!-- </template>-->
<!-- <el-icon>-->
<!-- <QuestionFilled color="#999999" />-->
<!-- </el-icon>-->
<!-- </el-tooltip>-->
<!-- </el-radio>-->
</el-radio-group>
</div>
<div class="item-wrap" v-if="formData.submit_after_action == 'text'">
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">{{ t('promptText') }}</div>
<div>
<el-radio-group v-model="formData.tips_type" class="!block">
<el-radio label="default" class="!block">
<span class="mr-[3px]">{{ t('defaultPrompt') }}</span>
<span class="!text-[#999] text-[12px] ml-[8px]">{{ t('defaultPromptTips') }}</span>
</el-radio>
<el-radio label="diy" class="!block">{{ t('diyPrompt') }}</el-radio>
</el-radio-group>
<el-input v-if="formData.tips_type == 'diy'" v-model.trim="formData.tips_text" :placeholder="t('tipsTextPlaceholder')" class="w-[350px]" maxlength="30" clearable show-word-limit />
</div>
</div>
<!-- 核销凭证 todo 后续完善 -->
<template v-else-if="formData.submit_after_action == 'voucher'">
<div class="item-wrap">
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">{{ t('validityPeriodOfVoucher') }}</div>
<div>
<el-radio-group v-model="formData.time_limit_type" class="!block">
<el-radio label="no_limit" class="!block">{{ t('noLimit') }}</el-radio>
<el-radio label="specify_time" class="!block">
<span class="mr-[3px]">{{ t('specifyTime') }}</span>
<el-tooltip effect="light" placement="top">
<template #content>
<p>{{ t('specifyTimeTips') }}</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</el-radio>
<el-radio label="submission_time" class="!block">
<span class="mr-[3px]">{{ t('submissionTime') }}</span>
<el-tooltip effect="light" placement="top">
<template #content>
<p class="w-[250px]">{{ t('submissionTimeTips') }}</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</el-radio>
</el-radio-group>
<el-date-picker v-if="formData.time_limit_type == 'specify_time'" v-model="formData.validity_time" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
<div class="flex items-center mt-[5px]" v-if="formData.time_limit_type == 'submission_time'">
<span>{{ t('afterSubmissionRecords') }}</span>
<!-- <div class="flex items-center px-[5px]">-->
<!-- v-model.trim="formData.length"-->
<el-input v-model.trim="formData.submission_time_value" @keyup="filterNumber($event)" size="small" clearable class="!w-[100px] px-[5px]" maxlength="3" />
<el-select v-model="formData.timeUnit" clearable class="!w-[100px] pr-[5px]" size="small">
<el-option v-for="(item, index) in validityOptions" :key="item.value" :label="item.text" :value="item.value"></el-option>
</el-select>
<span>{{ t('effective') }}</span>
</div>
</div>
</div>
<div class="item-wrap">
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">{{ t('voucherStyle') }}</div>
<div>
<el-form-item :label="t('titleAboveTheCode')">
<!-- v-model.trim="formData.active_name"-->
<el-input clearable :placeholder="t('titleAboveTheCodePlaceholder')" class="input-width" :maxlength="20" />
</el-form-item>
<el-form-item :label="t('contentAboveTheCode')">
<el-input clearable :placeholder="t('contentAboveTheCodePlaceholder')" class="input-width" :maxlength="20" />
<div>
<span class="text-primary cursor-pointer mr-[10px]">{{ t('addLinefeeds') }}</span>
<span class="text-primary cursor-pointer">{{ t('addFields') }}</span>
</div>
</el-form-item>
<el-form-item :label="t('contentBelowTheCode')">
<div class="block">
<el-checkbox class="!block" :label="t('submissionRecordTime')" value="" />
<el-checkbox class="!block !h-[20px]" :label="t('currentTime')" value="" />
<div class="text-[#999] ml-[22px]">{{ t('currentTimeTips') }}</div>
<el-checkbox class="!block" :label="t('dispalyPromptText')" value="" />
<el-input class="ml-[22px]" :rows="4" type="textarea" :placeholder="t('tipsTextPlaceholder')" maxlength="100" />
<el-checkbox class="!block" :label="t('voucherDeadline')" value="" />
<el-checkbox class="!block" :label="t('saveVoucher')" value="" />
</div>
</el-form-item>
</div>
</div>
</template>
<!-- todo 后续完善 -->
<div class="item-wrap">
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">
<span>{{ t('subsequentPperationButtons') }}</span>
<!-- <p class="text-[12px] text-[#999] mt-[4px] font-normal">最多选择2个</p>-->
</div>
<div class="content-list-wrap">
<!-- <el-checkbox-group :min="1" :max="2">-->
<!-- <el-checkbox v-model="formData.success_after_action.share" label="转发填写内容" value="share">-->
<!-- <div class="text-[#333]">转发填写内容</div>-->
<!-- </el-checkbox>-->
<!-- <p class="text-[#999] text-[12px] pl-[24px] mt-[4px]">提交表单后可转发给微信好友查看支持按钮文案自定义提醒填表人转发给特定人员查看</p>-->
<el-checkbox v-model="formData.success_after_action.finish" :label="t('finish')" value="finish">
<div class="text-[#333]">{{ t('finish') }}</div>
</el-checkbox>
<p class="text-[#999] text-[12px] pl-[24px] mt-[4px]">{{ t('finishTips') }}</p>
<el-checkbox v-model="formData.success_after_action.goback" :label="t('back')" value="goback">
<div class="text-[#333]">{{ t('back') }}</div>
</el-checkbox>
<p class="text-[#999] text-[12px] pl-[24px] mt-[4px]">{{ t('backTips') }}</p>
<!-- </el-checkbox-group>-->
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="confirm">{{ t('save') }}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/lang'
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import QRCode from 'qrcode'
import storage from '@/utils/storage'
import { filterNumber } from '@/utils/common'
import { getUrl } from '@/app/api/sys'
import { getFormSubmitConfig,editDiyFormSubmitConfig } from '@/app/api/diy_form'
const showDialog = ref(false)
const repeat = ref(false)
/**
* 表单数据
*/
const initialFormData = {
id: 0,
form_id: 0,
submit_after_action: 'text', // 填表人提交后操作text文字信息voucher核销凭证
tips_type: 'default', // 提示内容类型default默认提示diy自定义提示
tips_text: '', // 自定义提示内容
time_limit_type: 'no_limit', // 核销凭证有效期限制类型no_limit不限制specify_time指定固定开始结束时间submission_time按提交时间设置有效期
// 核销凭证时间限制规则json格式 todo 结构待定,后续完善
time_limit_rule: {
validity_time: [], // 指定固定开始结束时间
submission_time_value: '', // 按提交时间设置有效期
timeUnit: 'day', // 提交时间单位
},
// 核销凭证内容json格式 todo 结构待定,后续完善
voucher_content_rule: {},
// 填写成功后续操作
success_after_action: {
share: false, // 转发填写内容
finish: true, // 完成
goback: true, // 返回
}
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const wapUrl = ref('')
const wapDomain = ref('')
const wapImage = ref('')
const wapPreview = ref('')
const page = ref('')
// 核销凭证有效期
const validityOptions = reactive([
{
text:'天',
value:'day'
},
{
text:'周',
value:'week'
},
{
text:'月',
value:'month'
},
{
text:'年',
value:'year'
},
{
text:'分钟',
value:'minutes'
}
])
// getUrl().then((res: any) => {
// wapUrl.value = res.data.wap_url
//
// // 生产模式禁止
// if (import.meta.env.MODE == 'production') return
//
// wapDomain.value = res.data.wap_domain
//
// // env文件配置过wap域名
// if (wapDomain.value) {
// wapUrl.value = wapDomain.value + '/wap'
// }
//
// const wapDomainStorage = storage.get('wap_domain')
// if (wapDomainStorage) {
// wapUrl.value = wapDomainStorage
// }
// })
const loadQrcode = () => {
wapPreview.value = `${wapUrl.value}${page.value}`
// errorCorrectionLevel密度容错率LH(高)
QRCode.toDataURL(wapPreview.value, { errorCorrectionLevel: 'L', margin: 0, width: 120 }).then(url => {
wapImage.value = url
})
}
const emit = defineEmits(['complete'])
const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData)
if (row) {
const data = await (await getFormSubmitConfig(row.form_id)).data
if (data) {
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
}
// todo 靠后完善
// page.value = `/app/pages/index/diy_form?form_id=${formData.form_id}`
// loadQrcode()
}
}
/**
* 确认
*/
const confirm = () => {
if(formData.tips_type == 'diy' && !formData.tips_text){
ElMessage.error('提示不能为空')
return
}
if (repeat.value) return;
repeat.value = true
const data = formData
editDiyFormSubmitConfig(data).then(res => {
repeat.value = false
showDialog.value = false
emit('complete')
}).catch(err => {
repeat.value = false
})
}
defineExpose({
showDialog,
setFormData
})
</script>
<style lang="scss" scoped>
.preview-wrap {
position: relative;
width: 324px;
min-height: 555px;
padding: 82px 12px 20px;
background-size: 100%;
background-repeat: repeat-y;
background-image: url(../../../../app/assets/images/diy_form/mobile_line.png);
border-radius: 38px;
overflow: hidden;
box-shadow: none;
background-color: #fff !important;
margin-right: 24px;
overflow-y: auto;
.page-wrap {
position: relative;
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
text-align: center;
min-height: 548px;
height: auto;
}
.verify-voucher-wrap {
background-color: #f4f4f4;
.tips-wrap{
font-size: 15px;
font-weight: 400;
line-height: 21px;
color: rgba(0,0,0,.65);
margin: 20px 10px;
}
.qrcode-wrap{
border-radius: 12px;
margin: 0 20px;
background: #fff;
padding: 20px 10px 10px;
}
}
}
.item-wrap {
padding: 20px 24px 24px;
background-color: #fff;
border-radius: 2px;
display: flex;
position: relative;
&:after {
content: "";
display: block;
height: 1px;
width: calc(100% - 48px);
background-color: hsla(210, 8%, 51%, .13);
position: absolute;
left: 24px;
bottom: 0;
}
&:last-child:after {
display: none;
}
}
</style>

View File

@@ -0,0 +1,341 @@
<template>
<el-dialog v-model="showDialog" :title="t('writeSet')" width="600px" class="diy-dialog-wrap" :close-on-press-escape="true" :destroy-on-close="true" :close-on-click-modal="false">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<!-- <el-form-item :label="t('填写方式')">-->
<!-- <el-radio-group v-model="formData.write_way">-->
<!-- <el-radio label="no_limit">{{t('不限制')}}</el-radio>-->
<!-- <el-radio label="scan">{{t('仅限扫一扫')}}</el-radio>-->
<!-- <el-radio label="url">{{t('仅限链接进入')}}</el-radio>-->
<!-- </el-radio-group>-->
<!-- </el-form-item>-->
<el-form-item :label="t('joinMemberType')">
<el-radio-group v-model="formData.join_member_type">
<el-radio label="all_member">{{t('allMember')}}</el-radio>
<el-radio label="selected_member_level">{{t('selectedMemberLevel')}}</el-radio>
<el-radio label="selected_member_label">{{t('selectedMemberLabel')}}</el-radio>
</el-radio-group>
</el-form-item>
<!-- 会员标签 -->
<el-form-item :label="t('memberLabel')" prop="label_ids" v-if="formData.join_member_type=='selected_member_label'">
<el-select v-model="formData.label_ids" clearable multiple :placeholder="t('memberLabelPlaceholder')" class="input-width">
<el-option :label="item['label_name']" :value="item['label_id']" v-for="(item, index) in labelSelectData" :key="index" />
</el-select>
</el-form-item>
<!-- 会员等级 -->
<el-form-item :label="t('memberLevel')" prop="level_ids" v-if="formData.join_member_type=='selected_member_level'">
<el-select v-model="formData.level_ids" clearable multiple :placeholder="t('memberLevelPlaceholder')" class="input-width">
<el-option :label="item['level_name']" :value="item['level_id']" v-for="(item, index) in levelSelectData" :key="index" />
</el-select>
</el-form-item>
<el-form-item :label="t('apieceFillQuantity')" :class="{ '!mb-[5px]' : formData.member_write_type == 'diy' }">
<el-radio-group v-model="formData.member_write_type">
<el-radio label="no_limit">{{t('noLimit')}}</el-radio>
<el-radio label="diy">{{t('diy')}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label=" " v-if="formData.member_write_type == 'diy'" prop="member_write_rule">
<div class="flex items-center">
<span></span>
<el-input v-model.trim="formData.member_write_rule.time_value" @keyup="filterNumber($event)" size="small" class="!w-[50px] px-[5px]" maxlength="3" />
<el-select v-model="formData.member_write_rule.time_unit" class="!w-[60px] pr-[5px]" size="small">
<el-option v-for="(item, index) in validityOptions" :key="item.value" :label="item.text" :value="item.value"></el-option>
</el-select>
<span>可填写</span>
<el-input v-model.trim="formData.member_write_rule.num" @keyup="filterNumber($event)" size="small" class="!w-[50px] px-[5px]" maxlength="3" />
<span></span>
</div>
</el-form-item>
<el-form-item :label="t('fillQuantityTotal')" :class="{ '!mb-[5px]' : formData.form_write_type == 'diy' }">
<el-radio-group v-model="formData.form_write_type">
<el-radio label="no_limit">{{t('noLimit')}}</el-radio>
<el-radio label="diy">{{t('diy')}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label=" " v-if="formData.form_write_type == 'diy'" prop="form_write_rule">
<div class="flex items-center">
<span></span>
<el-input v-model.trim="formData.form_write_rule.time_value" @keyup="filterNumber($event)" size="small" class="!w-[50px] px-[5px]" maxlength="3" />
<el-select v-model="formData.form_write_rule.time_unit" class="!w-[60px] pr-[5px]" size="small">
<el-option v-for="(item, index) in validityOptions" :key="item.value" :label="item.text" :value="item.value"></el-option>
</el-select>
<span>可填写</span>
<el-input v-model.trim="formData.form_write_rule.num" @keyup="filterNumber($event)" size="small" class="!w-[50px] px-[5px]" maxlength="3" />
<span class="mr-[5px]"></span>
<el-tooltip effect="light" placement="top">
<template #content>
<p class="w-[250px]">{{ t('writeTips') }}</p>
</template>
<el-icon>
<QuestionFilled color="#999999" />
</el-icon>
</el-tooltip>
</div>
</el-form-item>
<el-form-item :label="t('fillInTheTimePeriod')" prop="time_limit_rule">
<el-radio-group v-model="formData.time_limit_type">
<el-radio label="no_limit">{{t('noLimit')}}</el-radio>
<el-radio label="specify_time">{{t('setSpecifyTime')}}</el-radio>
<el-radio label="open_day_time">{{t('openDayTime')}}</el-radio>
</el-radio-group>
<el-date-picker v-if="formData.time_limit_type == 'specify_time'" v-model="formData.time_limit_rule.specify_time" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
<div class="flex items-center mt-[5px]" v-if="formData.time_limit_type == 'open_day_time'">
<span class="mr-[5px]">每天</span>
<el-time-picker class="!w-[180px]" v-model="formData.time_limit_rule.open_day_time" format="HH:mm" value-format="HH:mm" is-range range-separator="-" start-placeholder="开始时间" end-placeholder="结束时间" />
<span class="ml-[5px]">可填写</span>
</div>
</el-form-item>
<!-- <el-form-item :label="t('允许修改内容')" class="display-block">-->
<!-- <el-switch v-model="formData.is_allow_update_content" :active-value="1" :inactive-value="0" />-->
<!-- <div class="text-sm text-gray-400">{{ t('开启后,填表人可以修改自己填写的内容。') }}</div>-->
<!-- </el-form-item>-->
<!-- <el-form-item :label="t('填写须知')">-->
<!-- <el-input v-model.trim="formData.write_instruction" :placeholder="t('请输入填写须知')" type="textarea" maxlength="500" show-word-limit rows="5" class="w-[400px]" clearable />-->
<!-- </el-form-item>-->
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { filterNumber } from '@/utils/common'
import {getMemberLabelAll,getMemberLevelAll } from '@/app/api/member'
import { getFormWriteConfig,editDiyFormWriteConfig } from '@/app/api/diy_form'
const showDialog = ref(false)
const loading = ref(false)
/**
* 表单数据
*/
const initialFormData = {
id: 0,
form_id: 0, // 万能表单id
write_way: 'no_limit', // 填写方式no_limit不限制scan仅限微信扫一扫url仅限链接进入
join_member_type: 'all_member', // 参与会员all_member所有会员参与selected_member_level指定会员等级selected_member_label指定会员标签
level_ids: [], // 会员等级id集合
label_ids: [], // 会员标签id集合
member_write_type: 'no_limit', // 每人可填写次数no_limit不限制diy自定义
// 每人可填写次数自定义规则
member_write_rule: {
time_value: 1, // 时间
time_unit: 'day', // 时间单位
num: 1 // 可填写次数
},
form_write_type: 'no_limit', // 表单可填写数量no_limit不限制diy自定义
// 表单可填写总数自定义规则
form_write_rule: {
time_value: 1, // 时间
time_unit: 'day', // 时间单位
num: 1 // 可填写次数
},
time_limit_type: 'no_limit', // 填写时间限制类型no_limit不限制specify_time指定开始结束时间open_day_time设置每日开启时间
// 填写时间限制规则
time_limit_rule: {
specify_time: [], // 指定开始结束时间
open_day_time: [], // 设置每日开启时间
},
is_allow_update_content: 0, // 是否允许修改自己填写的内容01
write_instruction: '', // 表单填写须知
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
label_ids: [
{ required: true, message: t('labelTips'), trigger: 'blur' }
],
level_ids: [
{ required: true, message: t('levelTips'), trigger: 'blur' }
],
member_write_rule: [
{
validator: (rule: any, value: string, callback: any) => {
let unit = ''
validityOptions.forEach((item,index)=>{
if(item.value == value.time_unit){
unit = item.text;
}
})
if(formData.member_write_type == 'diy'){
if(!value.time_value){
callback(new Error(`${unit}数不能为空`))
}else if(!value.num){
callback(new Error(t('numCannotNull')))
}else{
callback()
}
}else{
callback()
}
},
trigger: ['blur', 'change']
}
],
form_write_rule: [
{
validator: (rule: any, value: string, callback: any) => {
let unit = ''
validityOptions.forEach((item,index)=>{
if(item.value == value.time_unit){
unit = item.text;
}
})
if(formData.member_write_type == 'diy'){
if(!value.time_value){
callback(new Error(`${unit}数不能为空`))
}else if(!value.num){
callback(new Error(t('numCannotNull')))
}else{
callback()
}
}else{
callback()
}
},
trigger: ['blur', 'change']
}
],
time_limit_rule: [
{
validator: (rule: any, value: string, callback: any) => {
if (formData.time_limit_type == 'specify_time' && (!value.specify_time || !value.specify_time.length)) {
callback(new Error(t('timeLimitRuleOne')))
} else if (formData.time_limit_type == 'open_day_time' && (!value.open_day_time || !value.open_day_time.length)) {
callback(new Error(t('timeLimitRuleTwo')))
} else if (formData.time_limit_type == 'open_day_time' && value.open_day_time && value.open_day_time.length) {
if (value.open_day_time[0] == value.open_day_time[1]) {
callback(new Error(t('timeLimitRuleThree')))
} else {
callback()
}
} else {
callback()
}
},
trigger: ['blur', 'change']
}
]
}
})
const levelSelectData = ref([])
const labelSelectData = ref([])
// 获取全部标签
getMemberLabelAll().then(({ data }) => {
labelSelectData.value = data
})
getMemberLevelAll().then(({ data }) => {
levelSelectData.value = data
})
const validityOptions = reactive([
{
text:'天',
value:'day'
},
{
text:'周',
value:'week'
},
{
text:'月',
value:'month'
},
{
text:'年',
value:'year'
}
])
const emit = defineEmits(['complete'])
const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData)
loading.value = true
if (row) {
const data = await (await getFormWriteConfig(row.form_id)).data
if (data && Object.keys(data).length) {
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
}else{
formData.form_id = row.form_id;
}
}
loading.value = false
}
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
editDiyFormWriteConfig(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(err => {
loading.value = false
})
}
})
}
const filterSpecial = (event:any) => {
event.target.value = event.target.value.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '')
event.target.value = event.target.value.replace(/[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、]/g, '')
}
defineExpose({
showDialog,
setFormData
})
</script>
<style lang="scss" scoped></style>
<style lang="scss">
.diy-dialog-wrap .el-form-item__label{
height: auto !important;
}
.display-block {
.el-form-item__content {
display: block;
}
}
</style>