Merge branch 'feature/feature-20250331-h5' of https://e.coding.yili.com/yldc/ylst/ylst-survey-h5 into feature/feature-20250331-h5

This commit is contained in:
liu.huiying@ebiz-digits.com
2025-03-14 10:57:49 +08:00
26 changed files with 893 additions and 306 deletions

View File

@@ -1,5 +1,5 @@
# .env.development
VITE_APP_BASEURL=https://yls-api-uat.dctest.digitalyili.com/
VITE_APP_BASEURL=https://yls-api-uat.dctest.digitalyili.com
VITE_APP_ENV=development
VITE_APP_CURRENTMODE=dev
VITE_APP_BASEOSS=https://diaoyan-files.automark.cc

View File

@@ -1,5 +1,5 @@
# .env.development
VITE_APP_BASEURL=https://yls-api-uat.dctest.digitalyili.com/api/
VITE_APP_BASEURL=https://yls-api-uat.dctest.digitalyili.com
VITE_APP_ENV=uat
VITE_APP_CURRENTMODE=uat
VITE_APP_BASEOSS=https://diaoyan-files.automark.cc

3
components.d.ts vendored
View File

@@ -11,7 +11,6 @@ declare module 'vue' {
RichText: typeof import('./src/components/RichText.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
'Van-': typeof import('vant/es')['-']
VanActionSheet: typeof import('vant/es')['ActionSheet']
VanButton: typeof import('vant/es')['Button']
VanCell: typeof import('vant/es')['Cell']
@@ -20,6 +19,7 @@ declare module 'vue' {
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
VanCol: typeof import('vant/es')['Col']
VanDivider: typeof import('vant/es')['Divider']
VanFeild: typeof import('vant/es')['Feild']
VanField: typeof import('vant/es')['Field']
VanGrid: typeof import('vant/es')['Grid']
VanGridItem: typeof import('vant/es')['GridItem']
@@ -35,6 +35,7 @@ declare module 'vue' {
VanSwitch: typeof import('vant/es')['Switch']
VanTabbar: typeof import('vant/es')['Tabbar']
VanTabbarItem: typeof import('vant/es')['TabbarItem']
YLCascader: typeof import('./src/components/YLCascader.vue')['default']
YLPicker: typeof import('./src/components/YLPicker.vue')['default']
YLSelect: typeof import('./src/components/YLSelect.vue')['default']
}

View File

@@ -13,6 +13,9 @@
"lint": "npx eslint -c ./node_modules/@yl/yili-fe-lint-config/eslintrc.vue3.js \"src/**/*.{js,ts,tsx,vue,html}\" --fix",
"stylelint": "npx stylelint -c ./node_modules/@yl/yili-fe-lint-config/stylelintrc.js \"**/*.{css,scss,sass,less,vue,html}\" --fix"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-musl": "*"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.8.2",

View File

@@ -11,6 +11,10 @@ a,
transition: 0.4s;
}
.ml10 {
margin-left: 10px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160deg, 100%, 37%, 0.2);

View File

@@ -0,0 +1,57 @@
<script setup>
import { ref, watch } from 'vue';
import 'element-plus/dist/index.css';
import { ElCascader } from 'element-plus';
const props = defineProps({
options: {
type: Array,
default: () => []
},
modelValue: {
type: Array,
default: () => []
}
});
const cascader = ref(props.modelValue);
const emit = defineEmits(['update:modelValue']);
// 直接使用 toRef 保持响应式
const selectedValue = ref(props.modelValue);
watch(
() => props.modelValue,
(newVal) => {
selectedValue.value = newVal;
cascader.value = newVal;
}
);
const changeValue = (value) => {
const hasEmpty = value.some((item) => item[0] === '0');
const propEmpty = cascader.value.some((item) => item[0] === '0');
if (hasEmpty && !propEmpty) {
emit('update:modelValue', [['0']]);
} else {
emit(
'update:modelValue',
value.filter((item) => item[0] !== '0')
);
}
};
</script>
<template>
<el-cascader
v-model="selectedValue"
class="yl-cascader"
style="width: 100%"
:show-all-levels="false"
:options="props.options"
:props="{ multiple: true }"
@change="changeValue"
>
</el-cascader>
</template>
<style scoped>
::v-deep .yl-cascader {
width: 100%;
}
</style>

View File

@@ -84,7 +84,9 @@ const getType = (v: any) => {
return Object.prototype.toString.call(v).slice(8, -1).toLowerCase();
};
// 返回指定格式的日期时间
const getDateByFormat = (date: Date | string, fmt: dateFormat) => {
const getDateByFormat = (dateParam: Date | string, fmt: dateFormat) => {
// 避免直接修改参数
let date = dateParam;
const thisDateType = getType(date);
if (date === '' || (thisDateType !== 'date' && thisDateType !== 'string')) {
date = new Date();
@@ -100,7 +102,6 @@ const getDateByFormat = (date: Date | string, fmt: dateFormat) => {
const m = date.getMinutes();
const s = date.getSeconds();
// 个位数补0
// 月份比实际获取的少1所以要加1
const _M = M < 10 ? `0${M}` : M.toString();
const _D = D < 10 ? `0${D}` : D.toString();
const _h = h < 10 ? `0${h}` : h.toString();
@@ -116,13 +117,15 @@ const getDateByFormat = (date: Date | string, fmt: dateFormat) => {
};
// 比较两个日期大小
const dateRangeLegal = (sDate: string | Date, eDate: string | Date) => {
const dateRangeLegal = (sDateParam: string | Date, eDateParam: string | Date) => {
// 避免直接修改参数
let sDate = sDateParam;
let eDate = eDateParam;
if (!sDate || !eDate) {
return false;
}
// 补全模板
const complateStr = '0000-01-01 00:00:00';
// 兼容ios
if (typeof sDate === 'string') {
sDate = (sDate + complateStr.slice(sDate.length)).replace(/-/g, '/');
}
@@ -167,8 +170,8 @@ const getMaxDateLimit = computed(() => {
props.format
);
const tempStr = '0000-12-31 23:59:59';
const result
= props.maxDate.length !== 0 && thisMax.length > props.maxDate.length
const result =
props.maxDate.length !== 0 && thisMax.length > props.maxDate.length
? thisMax.slice(0, props.maxDate.length) + tempStr.slice(props.maxDate.length)
: thisMax;
return result.slice(0, props.format.length);
@@ -191,8 +194,8 @@ function onChange({ selectedValues, columnIndex }) {
renderMinuteColumns,
renderSecondColumns
];
updateColumns[columnIndex]
&& updateColumns[columnIndex](changeValue, getMinDateLimit.value, getMaxDateLimit.value, false);
updateColumns[columnIndex] &&
updateColumns[columnIndex](changeValue, getMinDateLimit.value, getMaxDateLimit.value, false);
}
// 渲染全部列
@@ -318,15 +321,16 @@ const renderMonthColumns = (
s: string,
e: string,
isFirst: boolean,
outRange: boolean = false
outRangeParam: boolean = false
) => {
const thisY = Number(v.slice(0, 4)); // 获取当前月
const thisM = Number(v.slice(5, 7)); // 获取当前月
const minY = Number(s.slice(0, 4)); // 最小年份
const maxY = Number(e.slice(0, 4)); // 最大年份
const listArr: any = []; // 获取月份数组
let forStart = -1; // 最小月份
let forEnd = -1; // 最小月份
let outRange = outRangeParam;
const thisY = Number(v.slice(0, 4));
const thisM = Number(v.slice(5, 7));
const minY = Number(s.slice(0, 4));
const maxY = Number(e.slice(0, 4));
const listArr: any = [];
let forStart = -1;
let forEnd = -1;
if (thisY === minY && thisY === maxY) {
forStart = Number(s.slice(5, 7));
forEnd = Number(e.slice(5, 7));
@@ -389,24 +393,25 @@ const renderDayColumns = (
s: string,
e: string,
isFirst: boolean,
outRange: boolean = false
outRangeParam: boolean = false
) => {
const thisYM = v.slice(0, 7); // 获取当前年月
const thisD = Number(v.slice(8, 10)); // 获取当前日
const startYM = s.slice(0, 7); // 开始时间临界值
const endYM = e.slice(0, 7); // 结束时间临界值
const listArr: any = []; // 获取月份数组
let forStart = -1; // 最小月份
let forEnd = -1; // 最小月份
let outRange = outRangeParam;
const thisYM = v.slice(0, 7);
const thisD = Number(v.slice(8, 10));
const startYM = s.slice(0, 7);
const endYM = e.slice(0, 7);
const listArr: any = [];
let forStart = -1;
let forEnd = -1;
if (thisYM === startYM && thisYM === endYM) {
forStart = Number(s.slice(8, 10)); // 开始时间的天临界值
forEnd = Number(e.slice(8, 10)); // 结束时间的天临界值
forStart = Number(s.slice(8, 10));
forEnd = Number(e.slice(8, 10));
} else if (thisYM === startYM) {
forStart = Number(s.slice(8, 10));
forEnd = getCountDays(Number(v.slice(0, 4)), Number(v.slice(5, 7)));
} else if (thisYM === endYM) {
forStart = 1;
forEnd = Number(e.slice(8, 10)); // 结束时间的天临界值
forEnd = Number(e.slice(8, 10));
} else {
forStart = 1;
forEnd = getCountDays(Number(v.slice(0, 4)), Number(v.slice(5, 7)));
@@ -460,18 +465,24 @@ const renderHourColumns = (
s: string,
e: string,
isFirst: boolean,
outRange: boolean = false
outRangeParam: boolean = false
) => {
const thisYMD = v.slice(0, 10); // 获取当前年月日
const startYMD = s.slice(0, 10); // 开始时间临界值
const endYMD = e.slice(0, 10); // 结束时间临界值
const thisH = Number(v.slice(11, 13)); // 获取当前小时
const listArr: any = []; // 获取小时数组
let forStart = -1; // 最小月份
let forEnd = -1; // 最小月份
// 避免直接修改参数
let outRange = outRangeParam;
// 获取当前年月日
const thisYMD = v.slice(0, 10);
// 开始时间临界值
const startYMD = s.slice(0, 10);
// 结束时间临界值
const endYMD = e.slice(0, 10);
// 获取当前小时
const thisH = Number(v.slice(11, 13));
const listArr: any = [];
let forStart = -1;
let forEnd = -1;
if (thisYMD === startYMD && thisYMD === endYMD) {
forStart = Number(s.slice(11, 13)); // 开始时间的小时临界值
forEnd = Number(e.slice(11, 13)); // 结束时间的小时临界值
forStart = Number(s.slice(11, 13));
forEnd = Number(e.slice(11, 13));
} else if (thisYMD === startYMD) {
forStart = Number(s.slice(11, 13));
forEnd = 23;
@@ -531,15 +542,20 @@ const renderMinuteColumns = (
s: string,
e: string,
isFirst: boolean,
outRange: boolean = false
outRangeParam: boolean = false
) => {
const thisYMDH = v.slice(0, 13); // 获取当前年月日小时
const startYMDH = s.slice(0, 13); // 开始时间临界值
const endYMDH = e.slice(0, 13); // 结束时间临界值
const thisM = Number(v.slice(14, 16)); // 获取当前分钟
const listArr: any = []; // 获取数组
let forStart = -1; // 循环最小值
let forEnd = -1; // 循环最大
// 避免直接修改参数
let outRange = outRangeParam;
// 获取当前年月日小时
const thisYMDH = v.slice(0, 13);
// 开始时间临界值
const startYMDH = s.slice(0, 13);
// 结束时间临界
const endYMDH = e.slice(0, 13);
const thisM = Number(v.slice(14, 16));
const listArr: any = [];
let forStart = -1;
let forEnd = -1;
if (thisYMDH === startYMDH && thisYMDH === endYMDH) {
forStart = Number(s.slice(14, 16));
forEnd = Number(e.slice(14, 16));
@@ -602,15 +618,16 @@ const renderSecondColumns = (
s: string,
e: string,
isFirst: boolean,
outRange: boolean = false
outRangeParam: boolean = false
) => {
const thisYMDHM = v.slice(0, 16); // 获取当前年月日小时
const startYMDHM = s.slice(0, 16); // 开始时间临界值
const endYMDHM = e.slice(0, 16); // 结束时间临界值
const thisS = Number(v.slice(17, 19)); // 获取当前分钟
const listArr: any = []; // 获取数组
let forStart = -1; // 循环最小值
let forEnd = -1; // 循环最大值
let outRange = outRangeParam;
const thisYMDHM = v.slice(0, 16);
const startYMDHM = s.slice(0, 16);
const endYMDHM = e.slice(0, 16);
const thisS = Number(v.slice(17, 19));
const listArr: any = [];
let forStart = -1;
let forEnd = -1;
if (thisYMDHM === startYMDHM && thisYMDHM === endYMDHM) {
forStart = Number(s.slice(17, 19));
forEnd = Number(e.slice(17, 19));
@@ -665,7 +682,7 @@ watch(
}
},
{
immediate: true // 立即监听--进入就会执行一次 监听显影状态
immediate: true
}
);

View File

@@ -88,7 +88,6 @@ const checkContains = (element, target) => {
try {
return element?.contains(target) ?? false;
} catch (e) {
console.error('Contains check failed:', e);
return false;
}
};
@@ -134,11 +133,11 @@ onMounted(() => {
bottom: 0;
left: 0;
z-index: 2008;
display: flex;
width: 100%;
height: 40px;
padding: 0 10px;
background: #fff;
display: flex;
line-height: 40px;
& button {

View File

@@ -10,7 +10,8 @@ import axios from 'axios';
const service = axios.create({
// baseURL: `${baseURL}`, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 30000 // request timeout
// request timeout
timeout: 30000
});
// request interceptor
@@ -36,7 +37,12 @@ service.interceptors.request.use(
// response interceptor
service.interceptors.response.use(
(response) => {
if (response.status === 200 || response.status === 201 || response.status === 202 || response.status === 204) {
if (
response.status === 200
|| response.status === 201
|| response.status === 202
|| response.status === 204
) {
if (response.config.method === 'put') {
// message.success('保存中...');
}

View File

@@ -407,7 +407,6 @@ export const useCommonStore = defineStore('common', {
}),
actions: {
async fetchQuestionInfo(questionInfo) {
console.log(questionInfo, 456);
try {
if (!questionInfo) return;

View File

@@ -7,44 +7,44 @@ export default {
title: '',
options: [
[
{
id: '',
is_fixed: 0,
is_other: 0,
is_remove_other: 0,
option: '<p>选项1</p>',
option_config: {
image_url: [],
title: '',
instructions: [],
option_type: 0,
limit_right_content: ''
},
option_index: 1,
parent_id: 0,
type: 0,
cascade: [],
config: []
},
{
id: '',
is_fixed: 0,
is_other: 0,
is_remove_other: 0,
option: '<p>选项2</p>',
option_config: {
image_url: [],
title: '',
instructions: [],
option_type: 0,
limit_right_content: ''
},
option_index: 1,
parent_id: 0,
type: 0,
cascade: [],
config: []
}
// {
// id: '',
// is_fixed: 0,
// is_other: 0,
// is_remove_other: 0,
// option: '<p>选项1</p>',
// option_config: {
// image_url: [],
// title: '',
// instructions: [],
// option_type: 0,
// limit_right_content: ''
// },
// option_index: 1,
// parent_id: 0,
// type: 0,
// cascade: [],
// config: []
// },
// {
// id: '',
// is_fixed: 0,
// is_other: 0,
// is_remove_other: 0,
// option: '<p>选项2</p>',
// option_config: {
// image_url: [],
// title: '',
// instructions: [],
// option_type: 0,
// limit_right_content: ''
// },
// option_index: 1,
// parent_id: 0,
// type: 0,
// cascade: [],
// config: []
// }
],
[
{

View File

@@ -12,9 +12,11 @@ const baseURL = config.default.proxyUrl;
// create an axios instance
const service = axios.create({
baseURL: `${baseURL}`, // url = base url + request url
// url = base url + request url
baseURL: `${baseURL}/api`,
// withCredentials: true, // send cookies when cross-domain requests
timeout: 30000 // request timeout
// request timeout
timeout: 30000
});
// request interceptor
@@ -24,10 +26,10 @@ service.interceptors.request.use(
config.headers.Accept = 'application/json';
}
config.headers.Authorization = `${localStorage.getItem('plantToken')}`;
if (!config.headers.remoteIp) {
config.baseURL += '/api';
}
delete config.headers.host;
// if (!config.headers.remoteIp) {
// config.baseURL += '/api';
// }
// delete config.headers.host;
config.headers.remoteIp = localStorage.getItem('plantIp') || '127.0.0.1';
// if (store.state.common.token) {
// config.headers['Login-Type'] = 'pc';
@@ -42,10 +44,10 @@ service.interceptors.request.use(
service.interceptors.response.use(
(response) => {
if (
response.status === 200
|| response.status === 201
|| response.status === 202
|| response.status === 204
response.status === 200 ||
response.status === 201 ||
response.status === 202 ||
response.status === 204
) {
if (response.config.method === 'put') {
// message.success('保存中...');

View File

@@ -7,7 +7,8 @@
*/
export function getSequenceName(names, list, options) {
const result = [];
const start = options?.start ?? 1; // 从数字几开始
// 从数字几开始
const start = options?.start ?? 1;
names.forEach((name) => {
const sequence = [];
@@ -61,7 +62,8 @@ export function getDomText(dom) {
* @returns {boolean[]}
*/
export function isRichRepeat(list, comparedList, options) {
const imageAsDifference = options?.imageAsDifference !== false; // 不比较图片。只要存在图片,则认为两段富文本不同
// 不比较图片。只要存在图片,则认为两段富文本不同
const imageAsDifference = options?.imageAsDifference !== false;
return list.map((rich) => {
return comparedList.some((compared) => {
@@ -83,7 +85,8 @@ export function isRichRepeat(list, comparedList, options) {
* @returns {boolean[]}
*/
export function isRichEmpty(list, options) {
const imageAsEmpty = options?.imageAsEmpty === true; // 不判断图片。只要存在图片,则认为两段富文本不为空
// 不判断图片。只要存在图片,则认为两段富文本不为空
const imageAsEmpty = options?.imageAsEmpty === true;
return list.map((rich) => {
if (!imageAsEmpty) {

View File

@@ -293,7 +293,6 @@ const actionEvent = (item, el) => {
const actionFun = {
// 单选事件 添加选项
radioAddOption: (element) => {
console.log(element);
element.options.map((item) => {
item.push({
id: uuidv4(),
@@ -313,6 +312,8 @@ const actionFun = {
});
});
element.last_option_index += 1;
saveQueItem(questionInfo.value.logics, [element]);
},
/**
@@ -320,16 +321,59 @@ const actionFun = {
* @param element {import('./components/Questions/types/martrix.js').MatrixSurveyQuestion}
*/
addMatrixRowOption: (element) => {
const optionIndex = element.last_option_index;
element.options[0].push({
option: '新增行'
cascade: [],
config: [],
is_fixed: 0,
is_other: 0,
is_remove_other: 0,
option: `<p style="text-align:center">行标签${element.options[0].length + 1}</p>`,
option_config: {
image_url: [],
title: '',
instructions: [],
option_type: 0,
limit_right_content: '<p>右极文字1</p>'
},
parent_id: 0,
type: 1,
id: uuidv4(),
option_index: optionIndex + 1
});
element.last_option_index = optionIndex + 1;
saveQueItem(questionInfo.value.logics, [element]);
},
/**
* martrix 矩阵列数增加
* @param element {import('./components/Questions/types/martrix.js').MatrixSurveyQuestion}
*/
addMatrixColumnOption: (element) => {
element.options[1].push({ option: '新增列' });
const optionIndex = element.last_option_index;
element.options[1].push({
cascade: [],
config: [],
is_fixed: 0,
is_other: 0,
is_remove_other: 0,
option: `<p style="text-align:center">列标签${element.options[1].length + 1}</p>`,
option_config: {
image_url: [],
title: '',
instructions: [],
option_type: 0,
limit_right_content: '<p>右极文字1</p>'
},
parent_id: 0,
type: 2,
id: uuidv4(),
option_index: optionIndex + 1
});
element.last_option_index = optionIndex + 1;
saveQueItem(questionInfo.value.logics, [element]);
}
};

View File

@@ -44,6 +44,7 @@
></van-switch>
</template>
</van-cell>
<van-divider></van-divider>
<van-cell title="题前隐藏" :border="false" @click="questionSetting('before')">
<template #right-icon>
@@ -69,13 +70,25 @@
:config="activeQuestion.config"
@update-config="updateConfig"
></rate-question-action>
<!-- 填空-->
<completion-question-action
v-if="activeQuestion.question_type === 4"
v-if="[4, 8].includes(activeQuestion.question_type)"
v-model="activeQuestion"
@save-option="saveSettings"
></completion-question-action>
<field-upload-question-action
v-if="activeQuestion.question_type === 18"
v-model="activeQuestion"
@save-option="saveSettings"
></field-upload-question-action>
<!-- 矩阵题目-->
<martrix-question-action
v-if="[8, 9, 10].includes(activeQuestion.question_type)"
v-model="activeQuestion"
@save-option="saveSettings"
></martrix-question-action>
</van-cell-group>
</van-action-sheet>
<!-- 移动 复制-->
@@ -125,6 +138,8 @@ import { storeToRefs } from 'pinia';
import QuestionBefore from '@/views/Design/components/ActionCompoents/components/QuestionItemAction/QuestionBefore.vue';
import RateQuestionAction from './components/QuestionItemAction/RateQuestionAction.vue';
import CompletionQuestionAction from '@/views/Design/components/ActionCompoents/components/QuestionItemAction/CompletionQuestionAction.vue';
import FieldUploadQuestionAction from '@/views/Design/components/ActionCompoents/components/QuestionItemAction/FieldUploadQuestionAction.vue';
import MartrixQuestionAction from '@/views/Design/components/ActionCompoents/components/QuestionItemAction/MartrixQuestionAction.vue';
import { v4 as uuidv4 } from 'uuid';
const store = useCounterStore();
const { questionsInfo } = storeToRefs(store);
@@ -238,8 +253,8 @@ const getSkipTypeText = (skipType) => {
const ls = [];
logics.map((item) => {
if (
item.skip_type === skipType
&& item.question_index === activeQuestion.value.question_index
item.skip_type === skipType &&
item.question_index === activeQuestion.value.question_index
) {
ls.push(item);
}
@@ -255,13 +270,13 @@ const getSkipTypeText = (skipType) => {
const questionSetting = (type) => {
switch (type) {
case 'before':
questionBeforeShow.value = true;
case 'before':
questionBeforeShow.value = true;
break;
case 'after':
questionBeforeShow.value = true;
break;
break;
case 'after':
questionBeforeShow.value = true;
break;
}
skipType.value = type === 'before' ? 1 : 0;
};

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request.js';
export function getFileType() {
return request({
url: `/console/file_type`,
method: 'get'
});
}

View File

@@ -68,7 +68,6 @@
</template>
</van-field>
</van-cell-group>
<van-divider></van-divider>
<van-cell
v-if="![5, 6, 7].includes(actionQuestion.config.text_type)"

View File

@@ -0,0 +1,182 @@
<template>
<van-cell title="文件数量" :border="false" label-align="left"> </van-cell>
<van-field
v-model="actionQuestion.config.min_number"
label="最少"
type="number"
:border="false"
label-align="left"
input-align="right"
class="action-field"
placeholder="不限"
@blur="emit('saveOption')"
@update:model-value="
(value) => {
actionQuestion.config.min_number = Number(value);
}
"
>
<template #right-icon>
<span>个</span>
</template>
</van-field>
<van-field
v-model="actionQuestion.config.max_number"
label="最多"
type="number"
:border="false"
placeholder="不限"
input-align="right"
class="action-field"
@blur="emit('saveOption')"
@update:model-value="
(value) => {
actionQuestion.config.max_number = Number(value);
}
"
>
<template #right-icon>
<span>个</span>
</template>
</van-field>
<van-divider></van-divider>
<van-cell title="文件大小" :border="false" label-align="left"> </van-cell>
<van-field
v-model="actionQuestion.config.min_size"
label="最少"
type="number"
:border="false"
label-align="left"
input-align="right"
class="action-field"
placeholder="不限"
@blur="emit('saveOption')"
@update:model-value="
(value) => {
actionQuestion.config.min_size = Number(value);
}
"
>
<template #right-icon>
<span>M</span>
</template>
</van-field>
<van-field
v-model="actionQuestion.config.max_size"
label="最多"
type="number"
:border="false"
placeholder="不限"
input-align="right"
class="action-field"
@blur="emit('saveOption')"
@update:model-value="
(value) => {
actionQuestion.config.max_size = Number(value);
}
"
>
<template #right-icon>
<span>M</span>
</template>
</van-field>
<van-divider></van-divider>
<van-field label="文件格式" :border="false" label-align="top">
<template #input>
<div style="width: 100%">
<YLCascader
v-if="fileTypeList.length > 0"
v-model="fileType"
:options="fileTypeList"
@update:model-value="changeFileType"
></YLCascader>
</div>
</template>
</van-field>
</template>
<script setup>
import { computed, defineEmits, onMounted, ref } from 'vue';
import YLCascader from '@/components/YLCascader.vue';
import { getFileType } from './Api/Index.js';
const props = defineProps({
modelValue: {
type: Object,
default: () => {
return {};
}
}
});
const emit = defineEmits(['update:modelValue', 'saveOption']);
const actionQuestion = computed({
get() {
return props.modelValue;
},
set(newValue) {
emit('update:modelValue', newValue);
}
});
const fileTypeList = ref([]);
const fileType = ref([]);
actionQuestion.value.config.file_type.split(',').map((item) => {
const type = item.split('-');
fileType.value.push(type);
});
const getFileTypeList = () => {
getFileType().then((res) => {
if (res.data) {
// 对象keymap
fileTypeList.value = Object.keys(res.data.data).map((key) => {
return {
label: key,
value: key,
children: res.data.data[key].map((item) => {
return {
label: item,
value: item
};
})
};
});
fileTypeList.value.unshift({
label: '不限',
value: '0',
isUnlimitSelected: 1,
children: null
});
}
});
};
const changeFileType = (value) => {
if (value === [['0']]) {
actionQuestion.value.config.file_type = '0';
emit('saveOption');
return;
}
const ls = [];
value.map((item) => {
const obj = [];
item.map((s) => {
obj.push(s);
});
ls.push(obj.join('-'));
});
actionQuestion.value.config.file_type = ls.join(',');
emit('saveOption');
};
onMounted(() => {
getFileTypeList();
});
</script>
<style scoped lang="scss">
.action-field {
& ::v-deep .van-field__label {
color: #bfbfbf;
font-size: 14px;
}
}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<van-cell v-if="[9, 10].includes(actionQuestion.question_type)" title="选项随机" :border="false">
<template #right-icon>
<van-switch
v-model="actionQuestion.config.select_random"
class="option-action-sheet-switch"
size="0.5rem"
:active-value="1"
:inactive-value="0"
@change="emit('saveOption')"
></van-switch>
</template>
</van-cell>
<van-field
v-if="
[9, 10].includes(actionQuestion.question_type) && actionQuestion.config.select_random === 1
"
v-model="actionQuestion.config.min_select"
label=""
:border="false"
label-align="left"
input-align="right"
class="action-field"
placeholder="不限"
@blur="emit('saveOption')"
@update:model-value="
(value) => {
actionQuestion.config.min_select = Number(value);
}
"
>
<template #input>
<div class="flex">
<van-checkbox
v-model="actionQuestion.config.row_random"
shape="square"
name="actionQuestion.config.row_random"
icon-size="0.5rem"
@change="emit('saveOption')"
>
行随机
</van-checkbox>
<van-checkbox
v-model="actionQuestion.config.cell_random"
class="ml10"
shape="square"
icon-size="0.5rem"
name="actionQuestion.config.cell_random"
@change="emit('saveOption')"
>
列随机
</van-checkbox>
</div>
</template>
</van-field>
<van-cell
title="右极文字"
type="number"
:border="false"
label-align="left"
input-align="right"
label-width="8em"
placeholder="不限"
readonly
@blur="emit('saveOption')"
>
<template #right-icon>
<van-switch
v-model="actionQuestion.config.is_limit_right_content"
:active-value="1"
:inactive-value="0"
size="0.5rem"
@change="emit('saveOption')"
></van-switch>
</template>
</van-cell>
<!-- 每行可选数量-->
<van-cell
v-if="[10].includes(actionQuestion.question_type)"
title="每行可选数量"
:border="false"
label-align="left"
>
</van-cell>
<van-cell-group v-if="[10].includes(actionQuestion.question_type)">
<van-field
v-model="actionQuestion.config.min_select"
label="最少"
type="number"
:border="false"
label-align="left"
input-align="right"
class="action-field"
placeholder="不限"
@blur="emit('saveOption')"
@update:model-value="
(value) => {
actionQuestion.config.min_select = Number(value);
}
"
>
<template #right-icon>
<span>个</span>
</template>
</van-field>
<van-field
v-model="actionQuestion.config.max_select"
label="最多"
type="number"
:border="false"
placeholder="不限"
input-align="right"
class="action-field"
@blur="emit('saveOption')"
@update:model-value="
(value) => {
actionQuestion.config.max_select = Number(value);
}
"
>
<template #right-icon>
<span>个</span>
</template>
</van-field>
</van-cell-group>
</template>
<script setup>
import { computed, defineEmits } from 'vue';
import { saveQuestion } from '@/api/design/index.js';
const props = defineProps({
modelValue: {
type: Object,
default: () => {
return {};
}
}
});
const emit = defineEmits(['update:modelValue', 'saveOption']);
const actionQuestion = computed({
get() {
return props.modelValue;
},
set(newValue) {
emit('update:modelValue', newValue);
}
});
</script>
<style scoped lang="scss">
.action-field {
& ::v-deep .van-field__label {
color: #bfbfbf;
font-size: 14px;
}
}
</style>

View File

@@ -183,7 +183,6 @@ skipOption.push(
let optionOptions = [];
// todo 不同题型逻辑对应不同 需要开发
const changeQuestionIndex = (value, logicItem) => {
console.log(logicItem);
if (!value) {
return [];
}
@@ -315,10 +314,8 @@ const symbolOptions = [
}
];
const getQuestionType = (value) => {
console.log(beforeQuesOptions);
const type = beforeQuesOptions.filter((item) => item.question_index === value)[0];
console.log(type);
const getQuestionType = () => {
// const type = beforeQuesOptions.filter((item) => item.question_index === value)[0];
};
const logicIf = (value, index) => {

View File

@@ -73,7 +73,7 @@
</template>
<script setup>
import OptionAction from '@/views/Design/components/ActionCompoents/OptionAction.vue';
import { defineAsyncComponent, toRefs } from 'vue';
import { defineAsyncComponent, toRefs, ref } from 'vue';
const choiceValue = ref('checked');
const Contenteditable = defineAsyncComponent(() => import('@/components/contenteditable.vue'));

View File

@@ -1,71 +1,89 @@
<script setup lang="ts">
import { Toast } from 'vant';
import { toRefs } from 'vue';
const { element } = defineProps<{ element: FileUploadQuestion }>();
const props = defineProps<{
element: any;
index: number;
active: boolean;
}>();
const { element } = toRefs(props);
/**
* 文件大小限制
* @property {number} max - 最大文件大小
* @property {number} min - 最小文件大小
*/
const fileLimit = {
// 默认3MB
max: 1024 * 1024 * 4,
min: 0
};
// const fileLimit = {
// // 默认3MB
// max: 1024 * 1024 * 4,
// min: 0
// };
/**
* 上传文件
* @description 上传文件
*/
function handleFileUpload() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
// fileInput.accept = '.jpg,.jpeg,.png,.gif';
// fileInput.multiple = true;
fileInput.click();
// function handleFileUpload() {
// const fileInput = document.createElement('input');
// fileInput.type = 'file';
// // fileInput.accept = '.jpg,.jpeg,.png,.gif';
// // fileInput.multiple = true;
// fileInput.click();
//
// fileInput.addEventListener('change', handleFileChange);
//
// function handleFileChange(event: Event) {
// const files = (event.target as HTMLInputElement).files;
// if (files) {
// for (let i = 0; i < files.length; i++) {
// const file = files[i];
// // console.log(file.size);
//
// if (file.size > fileLimit.max) {
// Toast.fail(`文件太大,超过${fileLimit.max / 1024 / 1024}MB`);
// return;
// } else if (file.size < fileLimit.min) {
// Toast.fail(`文件太小,小于${fileLimit.min / 1024 / 1024}MB`);
// return;
// }
// }
// }
// }
// }
fileInput.addEventListener('change', handleFileChange);
function handleFileChange(event: Event) {
const files = (event.target as HTMLInputElement).files;
if (files) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
// console.log(file.size);
if (file.size > fileLimit.max) {
Toast.fail(`文件太大,超过${fileLimit.max / 1024 / 1024}MB`);
return;
} else if (file.size < fileLimit.min) {
Toast.fail(`文件太小,小于${fileLimit.min / 1024 / 1024}MB`);
return;
}
}
}
}
}
const emit = defineEmits(['update:element']);
const emitValue = () => {
emit('update:element', element.value);
};
</script>
<template>
<van-cell-group>
<van-cell>
<van-field
v-model="element.stem"
:label="element.stem"
:required="element.config.is_required === 1"
label-align="top"
>
<template #left-icon>
{{ index + 1 }}
</template>
<!-- 使用 title 插槽来自定义标题 -->
<template #title>
<span>
<span v-if="element?.config?.is_required">*</span>
<span v-html="element.title"></span>
<span v-html="element.stem"></span>
</span>
<template #label>
<contenteditable
v-model="element.stem"
:active="active"
@blur="emitValue"
></contenteditable>
</template>
<template #label>
<div class="file-upload-label" @click="handleFileUpload">
<template #input>
<div class="file-upload-label">
<van-icon name="photo"></van-icon>
<span>上传文件</span>
</div>
</template>
</van-cell>
</van-field>
</van-cell-group>
</template>
@@ -76,5 +94,8 @@ function handleFileUpload() {
align-items: center;
width: 100%;
height: 50px;
padding: 5px;
border: 1px solid #dfdfdf;
border-radius: 3px;
}
</style>

View File

@@ -1,12 +1,17 @@
<script setup lang="ts">
import { useTemplateRef, type Directive } from 'vue';
import { useTemplateRef, toRefs } from 'vue';
const columnLabels = useTemplateRef<HTMLElement[]>('columnLabels');
// 注意, element.options 里面的东西是数组,第一项内容是行标签内容,第二项内容是列标签内容
// 类型 AI 生成 切勿盲目相信,以实际为准
const { element } = defineProps<{ element: MatrixSurveyQuestion }>();
const props = defineProps<{
element: Any;
index: number;
active: boolean;
}>();
const { element } = toRefs(props);
/**
* input 类型映射,里面自行处理逻辑返回对应类型
* // remark: 填空内容 question_type 8
@@ -15,41 +20,41 @@ const { element } = defineProps<{ element: MatrixSurveyQuestion }>();
* @default 'radio'
*/
const tableInputTypeMapping = (/** regx?: any */) => {
switch (element.question_type) {
case 8:
return 'text';
case 9:
return 'radio';
case 10:
return 'checkbox';
default:
return 'radio';
switch (element.value.question_type) {
case 8:
return 'text';
case 9:
return 'radio';
case 10:
return 'checkbox';
default:
return 'radio';
}
};
/**
* 自定义指令,用于在元素挂载后自动获取焦点
*/
const vFocus: Directive = {
mounted(el: HTMLInputElement) {
el.focus();
}
const emit = defineEmits(['update:element']);
const emitValue = () => {
emit('update:element', element.value);
};
</script>
<template>
<van-cell>
<van-field
v-model="element.stem"
:label="element.stem"
:required="element.config.is_required === 1"
label-align="top"
>
<template #left-icon>
{{ index + 1 }}
</template>
<!-- 使用 title 插槽来自定义标题 -->
<template #title>
<span>
<span v-if="element?.config?.is_required">*</span>
<span v-html="element.title"></span>
<span v-html="element.stem"></span>
</span>
<template #label>
<contenteditable v-model="element.stem" :active="active" @blur="emitValue"></contenteditable>
</template>
<!-- 使用 label 插槽来自定义标题 -->
<template #label>
<template #input>
<table class="martrix-table">
<thead>
<tr>
@@ -57,40 +62,43 @@ const vFocus: Directive = {
<th></th>
<!-- 第二行内容开始填充 -->
<td v-for="col in element.options[1]" :key="col.option" ref="columnLabels">
<!-- 编辑状态单次点击出输入框失焦后关闭 -->
<input
v-if="col.editor"
<contenteditable
v-model="col.option"
v-focus
type="text"
@focusout="col.editor = false"
/>
<span v-else @click="col.editor = true" v-html="col.option"></span>
:active="active"
@blur="emitValue"
></contenteditable>
</td>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="row in element.options[0]" :key="row.option">
<!-- 编辑状态单次点击出输入框失焦后关闭 -->
<td>
<input
v-if="row.editor"
<contenteditable
v-model="row.option"
v-focus
type="text"
@focusout="row.editor = false"
/>
<span v-else @click="row.editor = true" v-html="row.option"></span>
:active="active"
@blur="emitValue"
></contenteditable>
</td>
<td v-for="col in element.options[1]" :key="col.option">
<td v-for="col in element.options[1]" :key="col.option" class="td-input">
<!-- 编辑状态单次点击出输入框失焦后关闭 -->
<input :id="col.option" :type="tableInputTypeMapping()" :name="row.option" />
</td>
<td v-if="element.config.is_limit_right_content === 1">
<contenteditable
v-model="row.option_config.limit_right_content"
:active="active"
@blur="emitValue"
></contenteditable>
</td>
</tr>
</tbody>
</table>
</template>
</van-cell>
</van-field>
</template>
<style scoped lang="scss">
.martrix-table {
@@ -103,12 +111,31 @@ const vFocus: Directive = {
padding: 8px;
border: 1px solid #ddd;
border-width: 0 0 1px;
text-align: left;
text-align: center;
}
}
.td-input {
text-align: center;
}
input[type='text'] {
width: 85%;
border: none;
border-radius: 5px;
outline: 1px solid #ddd;
}
input[type='checkbox'] {
border: 1px solid #ddd;
border-radius: 5px;
outline: none;
}
input[type='radio'] {
border: 1px solid #ddd;
border-radius: 5px;
outline: none;
}
.martrix-table-action {

View File

@@ -52,8 +52,7 @@ const isOptionChecked = (rowIndex: number, colIndex: number): boolean => {
return !!props.matrixAnswer[key];
};
const handleRowNameChange = (value: string) => {
console.log(`row change: ${value}`);
const handleRowNameChange = () => {
// 你可以在这里添加其他逻辑
};

View File

@@ -5,7 +5,7 @@ const columnLabels = useTemplateRef<HTMLElement[]>('columnLabels');
// 注意, element.options 里面的东西是数组,第一项内容是行标签内容,第二项内容是列标签内容
// 类型 AI 生成 切勿盲目相信,以实际为准
const { element, index } = defineProps<{ element: MatrixSurveyQuestion, index: number }>();
const { element, index } = defineProps<{ element: MatrixSurveyQuestion; index: number }>();
const rowRecord = new Array(element.options[0].length);
// matrix 答案
@@ -23,14 +23,14 @@ const matrixAnswer = ref({
*/
const tableInputTypeMapping = (/** regx?: any */) => {
switch (element.question_type) {
case 8:
return 'text';
case 9:
return 'radio';
case 10:
return 'checkbox';
default:
return 'radio';
case 8:
return 'text';
case 9:
return 'radio';
case 10:
return 'checkbox';
default:
return 'radio';
}
};
@@ -50,7 +50,6 @@ const vFocus: Directive = {
*/
function handleRowNameChange(/* value: string */) {
// if (!value) return;
console.log(`row change`);
}
/**
@@ -58,69 +57,67 @@ function handleRowNameChange(/* value: string */) {
*/
function handleColNameChange(rowOption: string, colOption: string, e: any) {
// if (!value) return;
const col = element.options[0].findIndex(option => {
const col = element.options[0].findIndex((option) => {
return option.option === colOption;
});
const row = element.options[1].findIndex(option => {
const row = element.options[1].findIndex((option) => {
return option.option === rowOption;
});
console.log(`${col + 1}_${row + 1}`);
// 不同的 question_type 的 matrix 问卷处理不同的结果
switch (element.question_type) {
case 8: {
// 获取输入框元素
const inputElement = e.target as HTMLInputElement;
// 如果没有获取到输入框元素,则直接返回
if (!inputElement) return;
// 将输入框的值保存到 rowRecord 对应位置
rowRecord[col] = e!.target!.value;
// 清空 matrixAnswer 的 answer 属性
matrixAnswer.value.answer = {};
// 遍历所有行选项
element.options[0].forEach((_, rowIndex) => {
// 获取当前行记录
const colOptions = rowRecord[rowIndex];
// 如果当前行有记录,则更新 matrixAnswer 的 answer 属性
if (colOptions) {
matrixAnswer.value.answer[`R${rowIndex + 1}_C${col + 1}`] = colOptions;
}
});
break;
}
case 9:
// 将选择的行索引加1后保存到 rowRecord 对应位置
rowRecord[col] = row + 1;
// 清空 matrixAnswer 的 answer 属性
matrixAnswer.value.answer = {};
// 遍历 rowRecord更新 matrixAnswer 的 answer 属性
rowRecord.forEach((row, index) => {
matrixAnswer.value.answer[`${index + 1}_${row}`] = 1;
});
break;
case 10:
// 将选择的行索引加1后添加到 rowRecord 对应位置的数组中
rowRecord[col] = (rowRecord[col] || []).concat(row + 1);
// 清空 matrixAnswer 的 answer 属性
matrixAnswer.value.answer = {};
// 遍历所有行选项
element.options[0].forEach((rowOption, rowIndex) => {
// 获取当前行记录
const colOptions = rowRecord[rowIndex];
// 如果当前行有记录,则更新 matrixAnswer 的 answer 属性
if (colOptions) {
colOptions.forEach((col: any) => {
matrixAnswer.value.answer[`R${rowIndex + 1}_C${col}`] = true;
});
}
});
break;
default:
break;
case 8: {
// 获取输入框元素
const inputElement = e.target as HTMLInputElement;
// 如果没有获取到输入框元素,则直接返回
if (!inputElement) return;
// 将输入框的值保存到 rowRecord 对应位置
rowRecord[col] = e!.target!.value;
// 清空 matrixAnswer 的 answer 属性
matrixAnswer.value.answer = {};
// 遍历所有行选项
element.options[0].forEach((_, rowIndex) => {
// 获取当前行记录
const colOptions = rowRecord[rowIndex];
// 如果当前行有记录,则更新 matrixAnswer 的 answer 属性
if (colOptions) {
matrixAnswer.value.answer[`R${rowIndex + 1}_C${col + 1}`] = colOptions;
}
});
break;
}
case 9:
// 将选择的行索引加1后保存到 rowRecord 对应位置
rowRecord[col] = row + 1;
// 清空 matrixAnswer 的 answer 属性
matrixAnswer.value.answer = {};
// 遍历 rowRecord更新 matrixAnswer 的 answer 属性
rowRecord.forEach((row, index) => {
matrixAnswer.value.answer[`${index + 1}_${row}`] = 1;
});
break;
case 10:
// 将选择的行索引加1后添加到 rowRecord 对应位置的数组中
rowRecord[col] = (rowRecord[col] || []).concat(row + 1);
// 清空 matrixAnswer 的 answer 属性
matrixAnswer.value.answer = {};
// 遍历所有行选项
element.options[0].forEach((rowOption, rowIndex) => {
// 获取当前行记录
const colOptions = rowRecord[rowIndex];
// 如果当前行有记录,则更新 matrixAnswer 的 answer 属性
if (colOptions) {
colOptions.forEach((col: any) => {
matrixAnswer.value.answer[`R${rowIndex + 1}_C${col}`] = true;
});
}
});
break;
default:
break;
}
}
</script>
<template>
@@ -145,25 +142,38 @@ function handleColNameChange(rowOption: string, colOption: string, e: any) {
<td v-for="col in element.options[1]" :key="col.option" ref="columnLabels">
<!-- 编辑状态单次点击出输入框失焦后关闭 -->
<input
v-if="col.editor" v-model="col.option" v-focus type="text" @focusout="col.editor = false"
v-if="col.editor"
v-model="col.option"
v-focus
type="text"
@focusout="col.editor = false"
@click="handleRowNameChange(col.option!)"
/>
<span v-else @click="col.editor = true" v-html="col.option"></span>
</td>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(row) in element.options[0]" :key="row.option">
<tr v-for="row in element.options[0]" :key="row.option">
<!-- 编辑状态单次点击出输入框失焦后关闭 -->
<td>
<input v-if="row.editor" v-model="row.option" v-focus type="text" @focusout="row.editor = false" />
<input
v-if="row.editor"
v-model="row.option"
v-focus
type="text"
@focusout="row.editor = false"
/>
<span v-else @click="row.editor = true" v-html="row.option"></span>
</td>
<td v-for="col in element.options[1]" :key="col.option">
<!-- 编辑状态单次点击出输入框失焦后关闭 -->
<input
:id="col.option" :type="tableInputTypeMapping()" :name="row.option"
@change="handleColNameChange(col.option!, row.option!,$event)"
:id="col.option"
:type="tableInputTypeMapping()"
:name="row.option"
@change="handleColNameChange(col.option!, row.option!, $event)"
/>
</td>
</tr>

View File

@@ -1,5 +1,14 @@
<script setup lang="ts">
import { computed, onMounted, ref, useTemplateRef } from 'vue';
import { computed, onMounted, ref, useTemplateRef, toRefs } from 'vue';
const props = defineProps<{
element: any;
active: boolean;
index: number;
}>();
const { element, active } = toRefs(props);
const signatureCanvas = useTemplateRef('signatureCanvas');
const canvasWidth = ref(100);
@@ -63,6 +72,9 @@ onMounted(() => {
// 触摸开始,开始绘制适用于移动设备
signatureCanvas.value?.addEventListener('touchstart', (e) => {
if (!active.value) {
return;
}
// 防止页面滚动
e.preventDefault();
isDrawing = true;
@@ -150,25 +162,51 @@ const undo = () => {
ctx.putImageData(imageData, 0, 0);
}
};
const emit = defineEmits(['update:element']);
const emitValue = () => {
emit('update:element', element.value);
};
</script>
<template>
<van-cell>
<div class="sign-question">
<canvas
ref="signatureCanvas"
:width="canvasWidth"
:height="canvasHeight"
style="border: 1px solid #ccc; border-radius: 4px"
>
</canvas>
<div class="sign-text" :class="{ show: showSignText }">
<span @click="clearCanvas">清空</span>
<span @click="undo">撤销</span>
<span @click="togglePen">{{ isEraser ? '画笔' : '橡皮擦' }}</span>
<span @click="saveCanvas">完成并上传</span>
</div>
</div>
<van-field
:label="element.stem"
:required="element.config.is_required === 1"
label-align="top"
:border="false"
readonly
class="base-select"
>
<template #left-icon>
{{ index + 1 }}
</template>
<template #label>
<contenteditable
v-model="element.stem"
:active="active"
@blur="emitValue"
></contenteditable>
</template>
<template #input>
<div class="sign-question">
<canvas
ref="signatureCanvas"
:width="canvasWidth"
:height="canvasHeight"
style="border: 1px solid #ccc; border-radius: 4px"
>
</canvas>
<div class="sign-text" :class="{ show: showSignText }">
<span @click="clearCanvas">清空</span>
<span @click="undo">撤销</span>
<span @click="togglePen">{{ isEraser ? '画笔' : '橡皮擦' }}</span>
<span @click="saveCanvas">完成并上传</span>
</div>
</div>
</template>
</van-field>
</van-cell>
</template>