feat(css): 添加移动端 iconfont样式并优化内容编辑组件- 在 main.scss 中添加移动端 iconfont 样式

- 为 contenteditable 组件添加失焦事件处理
- 新增 YLPicker 组件,支持年月日时分秒的自定义格式选择
This commit is contained in:
陈昱达
2025-03-10 15:26:14 +08:00
parent 54c06913d6
commit d29ba27e93
25 changed files with 2186 additions and 163 deletions

9
components.d.ts vendored
View File

@@ -19,11 +19,18 @@ declare module 'vue' {
VanCheckbox: typeof import('vant/es')['Checkbox']
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
VanCol: typeof import('vant/es')['Col']
VanDatePicker: typeof import('vant/es')['DatePicker']
VanDatetimePicker: typeof import('vant/es')['DatetimePicker']
VanDialog: typeof import('vant/es')['Dialog']
VanDivider: typeof import('vant/es')['Divider']
VanFiel: typeof import('vant/es')['Fiel']
VanField: typeof import('vant/es')['Field']
VanGrid: typeof import('vant/es')['Grid']
VanGridItem: typeof import('vant/es')['GridItem']
VanIcon: typeof import('vant/es')['Icon']
VanNavBar: typeof import('vant/es')['NavBar']
VanPicker: typeof import('vant/es')['Picker']
VanPikcer: typeof import('vant/es')['Pikcer']
VanPopup: typeof import('vant/es')['Popup']
VanRadio: typeof import('vant/es')['Radio']
VanRadioGroup: typeof import('vant/es')['RadioGroup']
@@ -33,6 +40,8 @@ declare module 'vue' {
VanSwitch: typeof import('vant/es')['Switch']
VanTabbar: typeof import('vant/es')['Tabbar']
VanTabbarItem: typeof import('vant/es')['TabbarItem']
VanTimePicker: typeof import('vant/es')['TimePicker']
YLPicker: typeof import('./src/components/YLPicker.vue')['default']
YLSelect: typeof import('./src/components/YLSelect.vue')['default']
}
}

View File

@@ -1,6 +1,7 @@
@import 'base';
@import '../../fonts/iconfont.css';
@import 'vant';
@import '../../fonts/moblie/iconfont.css';
a,
.green {

698
src/components/YLPicker.vue Normal file
View File

@@ -0,0 +1,698 @@
<!-- vant的picker组件只支持年月日有些情况下需要一个能支持选择年月日时分秒的picker组件 -->
<!-- 本组件可支持年月日时分秒任意格式dateFormat -->
<template>
<!-- 弹出层 -->
<van-picker
ref="dateTimePicker"
v-model="data.selectedValues"
:title="title"
:columns="data.columns"
swipe-duration="500"
@cancel="cancelOn"
@confirm="onConfirm"
@change="onChange"
/>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue';
// 所有可能用到的日期格式
type dateFormat =
| 'YYYY'
| 'YYYY-MM'
| 'YYYY-MM-DD'
| 'YYYY-MM-DD HH'
| 'YYYY-MM-DD HH:mm'
| 'YYYY-MM-DD HH:mm:ss';
/** 声明props类型 */
interface Props {
// 传入的显示隐藏状态
showPicker: boolean;
// picker标题
title?: string;
// 传入的日期值
values: string;
// 自定义格式
format?: dateFormat;
// 自定义单位
units?: string[];
// 最小日期时间
minDate?: string;
// 最大日期时间
maxDate?: string;
}
interface IntColumnsItem {
text: string;
value: string;
children?: IntColumnsItem[];
}
const props = withDefaults(defineProps<Props>(), {
showPicker: false,
title: '请选择日期',
values: '',
format: 'YYYY-MM-DD HH:mm',
units: () => [],
minDate: '',
maxDate: ''
});
// 定义要向父组件传递的事件
const emit = defineEmits(['cancel', 'confirm']);
const dateTimePicker = ref(null);
// 定义数据
const data = reactive<{
isPicker: boolean;
columns: IntColumnsItem[];
selectedValues: any[];
tempSelectedValues: any[];
}>({
// 是否显示弹出层
isPicker: false,
// 所有时间列
columns: [],
// 控件选择的时间值
selectedValues: [],
// 临时数据,数据变化过程中选中的值
tempSelectedValues: []
});
// 获取数据类型
const getType = (v: any) => {
return Object.prototype.toString.call(v).slice(8, -1).toLowerCase();
};
// 返回指定格式的日期时间
const getDateByFormat = (date: Date | string, fmt: dateFormat) => {
const thisDateType = getType(date);
if (date === '' || (thisDateType !== 'date' && thisDateType !== 'string')) {
date = new Date();
} else if (thisDateType === 'string') {
date = new Date((date as string).replace(/-/g, '/'));
} else {
date = new Date(date);
}
const Y = date.getFullYear();
const M = date.getMonth() + 1;
const D = date.getDate();
const h = date.getHours();
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();
const _m = m < 10 ? `0${m}` : m.toString();
const _s = s < 10 ? `0${s}` : s.toString();
return fmt
.replace(/YYYY/g, Y.toString())
.replace('MM', _M)
.replace('DD', _D)
.replace('HH', _h)
.replace('mm', _m)
.replace('ss', _s);
};
// 比较两个日期大小
const dateRangeLegal = (sDate: string | Date, eDate: string | Date) => {
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, '/');
}
if (typeof eDate === 'string') {
eDate = (eDate + complateStr.slice(eDate.length)).replace(/-/g, '/');
}
return new Date(sDate) <= new Date(eDate);
};
/**
* 空闲位自动补全0
* @param num 数据
* @param len 设定长度
*/
const autoFillZero = (num: number, len: number) => {
return (Array(len).join('0') + num).slice(-len);
};
// 获取某年某月有多少天
const getCountDays = (year: number, month: number) => {
// 获取某年某月多少天
const day = new Date(year, month, 0);
return day.getDate();
};
// 获取最小时间范围
const getMinDateLimit = computed(() => {
return getDateByFormat(
props.minDate && props.minDate.length >= 0
? props.minDate
: `${new Date().getFullYear() - 10}-01-01 00:00:00`,
props.format
);
});
// 获取最大时间范围
const getMaxDateLimit = computed(() => {
const thisMax = getDateByFormat(
props.maxDate && props.maxDate.length >= 0
? props.maxDate
: `${new Date().getFullYear() + 10}-12-31 23:59:59`,
props.format
);
const tempStr = '0000-12-31 23:59:59';
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);
});
function onChange({
selectedValues,
columnIndex
}: {
// 所有列选中值
selectedValues: any[];
// 当前变化列索引
columnIndex: number;
}) {
// 更新渲染变化的列之后的所有列columnIndex
const splitArr = ['-', '-', ' ', ':', ':'];
let iiindex = -1;
const changeValue = selectedValues.reduce((a, b) => {
iiindex += 1;
return a + splitArr[iiindex] + b;
});
// 当前列变化后此时此刻时间
// 更新当前列之后的所有列(包括当前列)
const updateColumns: function[] = [
renderYearColumns,
renderMonthColumns,
renderDayColumns,
renderHourColumns,
renderMinuteColumns,
renderSecondColumns
];
updateColumns[columnIndex] &&
updateColumns[columnIndex](changeValue, getMinDateLimit.value, getMaxDateLimit.value, false);
}
// 渲染全部列
const getcolumns = () => {
// 先清空全部列
data.columns = [];
// 清空年月日时分秒时间值
data.tempSelectedValues = [];
data.selectedValues = [];
// 获取props.values转换成指定格式后的日期时间
const defaultDateTime = getDateByFormat(props.values, props.format);
let usefullDateTime = defaultDateTime;
if (!dateRangeLegal(getMinDateLimit.value, defaultDateTime)) {
usefullDateTime = getMinDateLimit.value;
} else if (!dateRangeLegal(defaultDateTime, getMaxDateLimit.value)) {
usefullDateTime = getMaxDateLimit.value;
}
// 渲染修正“年”列
renderYearColumns(usefullDateTime, getMinDateLimit.value, getMaxDateLimit.value, true);
};
/**
* 渲染年所在列,并自动修正在“年”这一列列范围之外的数据
* @param v 需要做比较的数据dateFormat格式
* @param s 比较条件开始时间临界值dateFormat格式
* @param e 比较条件结束时间临界值dateFormat格式
* @param isFirst 是否是第一次渲染,需要定位到当前
* @param outRange 是否超出范围之外
*/
const renderYearColumns = (v: string, s: string, e: string, isFirst: boolean) => {
// 设置年范围
// 获取前后十年数组
const listArr: any = [];
const Currentday = new Date().getFullYear();
// 最小月份
let forStart = 2020;
// 最小月份
let forEnd = 2040;
if (s && e) {
const startYearLimit = s.slice(0, 4);
const endYearLimit = e.slice(0, 4);
// 如果最小、最大日期设定的范围错误
if (!dateRangeLegal(s, e)) {
forStart = Currentday - 10;
forEnd = Currentday + 10;
} else {
if (startYearLimit === endYearLimit) {
forStart = Number(startYearLimit);
forEnd = Number(startYearLimit);
} else {
forStart = Number(startYearLimit);
forEnd = Number(endYearLimit);
}
}
} else if (s) {
const startYearLimit = s.slice(0, 4);
if (Currentday <= Number(startYearLimit)) {
forStart = Number(startYearLimit);
forEnd = Number(startYearLimit) + 10;
} else {
forStart = Number(startYearLimit);
forEnd = Currentday + 10;
}
} else if (e) {
const endYearLimit = e.slice(0, 4);
if (Currentday <= Number(endYearLimit)) {
forStart = Currentday - 10;
forEnd = Number(endYearLimit);
} else {
forStart = Number(endYearLimit) - 10;
forEnd = Number(endYearLimit);
}
} else {
forStart = Currentday - 10;
forEnd = Currentday + 10;
}
for (let m = forStart; m <= forEnd; m++) {
listArr.push({
text: `${m}${props.units[0] || ''}`,
value: m.toString()
});
}
// 判断当前“年”是否在以上合理范围之内
const thisYear = Number(v.slice(0, 4));
// 当前定位到的年份
let vmValue: string | number = '';
if (thisYear < forStart) {
vmValue = forStart;
} else if (thisYear > forEnd) {
vmValue = forEnd;
} else {
// 范围正确
vmValue = thisYear;
}
// 插入/更新到data中
if (isFirst) {
data.columns.push(listArr);
data.tempSelectedValues.push(autoFillZero(vmValue, 4));
} else {
data.columns[0] = listArr;
data.tempSelectedValues[0] = autoFillZero(vmValue, 4);
}
if (props.format.length >= 7) {
// 至少是“YYYY-MM”格式则渲染“月”
// 根据当前年渲染“月”这一列
renderMonthColumns(autoFillZero(vmValue, 4) + v.slice(4), s, e, isFirst);
} else {
// 确定选择结果
data.selectedValues = [...data.tempSelectedValues];
}
};
/**
* 渲染月所在列,并自动修正在“月”这一列列范围之外的数据
* @param v 需要做比较的数据dateFormat格式
* @param s 比较条件开始时间临界值dateFormat格式
* @param e 比较条件结束时间临界值dateFormat格式
* @param isFirst 是否是第一次渲染,需要定位到当前
* @param outRange 是否超出范围之外
*/
const renderMonthColumns = (
v: string,
s: string,
e: string,
isFirst: boolean,
outRange: 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; // 最小月份
if (thisY === minY && thisY === maxY) {
forStart = Number(s.slice(5, 7));
forEnd = Number(e.slice(5, 7));
} else if (thisY === minY) {
forStart = Number(s.slice(5, 7));
forEnd = 12;
} else if (thisY === maxY) {
forStart = 1;
forEnd = Number(e.slice(5, 7));
} else {
forStart = 1;
forEnd = 12;
}
for (let m = forStart; m <= forEnd; m++) {
listArr.push({
text: autoFillZero(m, 2) + (props.units[1] || ''),
value: autoFillZero(m, 2)
});
}
// 判断当前月是否在此范围之内
let vmValue: string | number = '';
if (thisM < forStart || outRange) {
vmValue = forStart;
outRange = true;
} else if (thisM > forEnd) {
vmValue = forEnd;
outRange = true;
} else {
// 范围正确
vmValue = thisM;
}
// 插入/更新到data中
if (isFirst) {
data.columns.push(listArr);
data.tempSelectedValues.push(autoFillZero(vmValue, 2));
} else {
data.columns[1] = listArr;
data.tempSelectedValues[1] = autoFillZero(vmValue, 2);
}
if (props.format.length >= 10) {
// 至少是“YYYY-MM-DD”格式则渲染“日”
// 根据当前年渲染“日”这一列
renderDayColumns(v.slice(0, 5) + autoFillZero(vmValue, 2) + v.slice(7), s, e, isFirst);
} else {
// 确定选择结果
data.selectedValues = [...data.tempSelectedValues];
}
};
/**
* 渲染日所在列,并自动修正在“日”这一列列范围之外的数据
* @param v 需要做比较的数据dateFormat格式
* @param s 比较条件开始时间临界值dateFormat格式
* @param e 比较条件结束时间临界值dateFormat格式
* @param isFirst 是否是第一次渲染,需要定位到当前
* @param outRange 是否超出范围之外
*/
const renderDayColumns = (
v: string,
s: string,
e: string,
isFirst: boolean,
outRange: 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; // 最小月份
if (thisYM === startYM && thisYM === endYM) {
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)); // 结束时间的天临界值
} else {
forStart = 1;
forEnd = getCountDays(Number(v.slice(0, 4)), Number(v.slice(5, 7)));
}
for (let m = forStart; m <= forEnd; m++) {
listArr.push({
text: autoFillZero(m, 2) + (props.units[2] || ''),
value: autoFillZero(m, 2)
});
}
// 判断当前日是否在此范围之内
let vmValue: string | number = '';
if (thisD < forStart || outRange) {
vmValue = forStart;
outRange = true;
} else if (thisD > forEnd) {
vmValue = forEnd;
outRange = true;
} else {
// 范围正确
vmValue = thisD;
}
// 插入/更新到data中
if (isFirst) {
data.columns.push(listArr);
data.tempSelectedValues.push(autoFillZero(vmValue, 2));
} else {
data.columns[2] = listArr;
data.tempSelectedValues[2] = autoFillZero(vmValue, 2);
}
if (props.format.length >= 13) {
// 至少是“YYYY-MM-DD HH”格式则渲染“时”
// 根据当前年渲染“日”这一列
renderHourColumns(v.slice(0, 8) + autoFillZero(vmValue, 2) + v.slice(10), s, e, isFirst);
} else {
// 确定选择结果
data.selectedValues = [...data.tempSelectedValues];
}
};
/**
* 渲染小时所在列,并自动修正在“时”这一列列范围之外的数据
* @param v 需要做比较的数据dateFormat格式
* @param s 比较条件开始时间临界值dateFormat格式
* @param e 比较条件结束时间临界值dateFormat格式
* @param isFirst 是否是第一次渲染,需要定位到当前
* @param outRange 是否超出范围之外
*/
const renderHourColumns = (
v: string,
s: string,
e: string,
isFirst: boolean,
outRange: 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; // 最小月份
if (thisYMD === startYMD && thisYMD === endYMD) {
forStart = Number(s.slice(11, 13)); // 开始时间的小时临界值
forEnd = Number(e.slice(11, 13)); // 结束时间的小时临界值
} else if (thisYMD === startYMD) {
forStart = Number(s.slice(11, 13));
forEnd = 23;
} else if (thisYMD === endYMD) {
forStart = 0;
forEnd = Number(e.slice(11, 13));
} else {
forStart = 0;
forEnd = 23;
}
for (let m = forStart; m <= forEnd; m++) {
listArr.push({
text: autoFillZero(m, 2) + (props.units[3] || ''),
value: autoFillZero(m, 2)
});
}
// 判断当前小时是否在此范围之内
let vmValue: string | number = '';
if (thisH < forStart || outRange) {
vmValue = forStart;
outRange = true;
} else if (thisH > forEnd) {
vmValue = forEnd;
outRange = true;
} else {
// 范围正确
vmValue = thisH;
}
// 插入/更新到data中
if (isFirst) {
data.columns.push(listArr);
data.tempSelectedValues.push(autoFillZero(vmValue, 2));
} else {
data.columns[3] = listArr;
data.tempSelectedValues[3] = autoFillZero(vmValue, 2);
}
if (props.format.length >= 16) {
// 至少是“YYYY-MM-DD HH:mm”格式则渲染“分”
// 根据当前年渲染“分”这一列
renderMinuteColumns(v.slice(0, 11) + autoFillZero(vmValue, 2) + v.slice(13), s, e, isFirst);
} else {
// 确定选择结果
data.selectedValues = [...data.tempSelectedValues];
}
};
/**
* 渲染分钟所在列,并自动修正在“分”这一列列范围之外的数据
* @param v 需要做比较的数据dateFormat格式
* @param s 比较条件开始时间临界值dateFormat格式
* @param e 比较条件结束时间临界值dateFormat格式
* @param isFirst 是否是第一次渲染,需要定位到当前
* @param outRange 是否超出范围之外
*/
const renderMinuteColumns = (
v: string,
s: string,
e: string,
isFirst: boolean,
outRange: 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; // 循环最大值
if (thisYMDH === startYMDH && thisYMDH === endYMDH) {
forStart = Number(s.slice(14, 16));
forEnd = Number(e.slice(14, 16));
} else if (thisYMDH === startYMDH) {
forStart = Number(s.slice(14, 16));
forEnd = 59;
} else if (thisYMDH === endYMDH) {
forStart = 0;
forEnd = Number(e.slice(14, 16));
} else {
forStart = 0;
forEnd = 59;
}
for (let m = forStart; m <= forEnd; m++) {
listArr.push({
text: autoFillZero(m, 2) + (props.units[4] || ''),
value: autoFillZero(m, 2)
});
}
// 判断当前小时是否在此范围之内
let vmValue: string | number = '';
if (thisM < forStart || outRange) {
vmValue = forStart;
outRange = true;
} else if (thisM > forEnd) {
vmValue = forEnd;
outRange = true;
} else {
// 范围正确
vmValue = thisM;
}
// 插入/更新到data中
if (isFirst) {
data.columns.push(listArr);
data.tempSelectedValues.push(autoFillZero(vmValue, 2));
} else {
data.columns[4] = listArr;
data.tempSelectedValues[4] = autoFillZero(vmValue, 2);
}
if (props.format.length === 19) {
// 至少是“YYYY-MM-DD HH:mm:ss”格式则渲染“秒”
// 根据当前年渲染“秒”这一列
renderSecondColumns(v.slice(0, 14) + autoFillZero(vmValue, 2) + v.slice(16), s, e, isFirst);
} else {
// 确定选择结果
data.selectedValues = [...data.tempSelectedValues];
}
};
/**
* 渲染秒钟所在列,并自动修正在“秒”这一列列范围之外的数据
* @param v 需要做比较的数据dateFormat格式
* @param s 比较条件开始时间临界值dateFormat格式
* @param e 比较条件结束时间临界值dateFormat格式
* @param isFirst 是否是第一次渲染,需要定位到当前
* @param outRange 是否超出范围之外
*/
const renderSecondColumns = (
v: string,
s: string,
e: string,
isFirst: boolean,
outRange: 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; // 循环最大值
if (thisYMDHM === startYMDHM && thisYMDHM === endYMDHM) {
forStart = Number(s.slice(17, 19));
forEnd = Number(e.slice(17, 19));
} else if (thisYMDHM === startYMDHM) {
forStart = Number(s.slice(17, 19));
forEnd = 59;
} else if (thisYMDHM === endYMDHM) {
forStart = 0;
forEnd = Number(e.slice(17, 19));
} else {
forStart = 0;
forEnd = 59;
}
for (let m = forStart; m <= forEnd; m++) {
listArr.push({
text: autoFillZero(m, 2) + (props.units[5] || ''),
value: autoFillZero(m, 2)
});
}
// 判断当前小时是否在此范围之内
let vmValue: string | number = '';
if (thisS < forStart || outRange) {
vmValue = forStart;
outRange = true;
} else if (thisS > forEnd) {
vmValue = forEnd;
outRange = true;
} else {
// 范围正确
vmValue = thisS;
}
// 插入/更新到data中
if (isFirst) {
data.columns.push(listArr);
data.tempSelectedValues.push(autoFillZero(vmValue, 2));
} else {
data.columns[5] = listArr;
data.tempSelectedValues[5] = autoFillZero(vmValue, 2);
}
// 确定选择结果
data.selectedValues = [...data.tempSelectedValues];
};
watch(
() => props.showPicker,
(val) => {
data.isPicker = val;
if (val) {
// console.log("当前最大最小值判断结果", getMinDateLimit.value, getMaxDateLimit.value);
// 每次显示前重新渲染全部列
getcolumns();
}
},
{
immediate: true // 立即监听--进入就会执行一次 监听显影状态
}
);
// 时间选择器关闭 值不改变并关闭弹框
function cancelOn() {
emit('cancel');
}
// 时间选择器确定 值改变
function onConfirm() {
// 注意data.selectedValues比selectedValues更准确
// 拼接数据
const splitArr = ['-', '-', ' ', ':', ':'];
let iiindex = -1;
const changeValue = data.selectedValues.reduce((a, b) => {
iiindex += 1;
return a + splitArr[iiindex] + b;
});
emit('confirm', changeValue);
}
</script>

View File

@@ -86,7 +86,11 @@ onMounted(() => {
editor.value.addEventListener('focus', () => {
showAction.value = true;
});
editor.value.addEventListener('blur', () => {
setTimeout(() => {
showAction.value = false;
});
});
document.addEventListener('resize', () => {
showAction.value = false;
});

View File

@@ -9,6 +9,14 @@
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont')
format('svg');
src:
url('//at.alicdn.com/t/c/font_4841764_vat2jbvw3q.woff2?t=1741575060989') format('woff2'),
url('//at.alicdn.com/t/c/font_4841764_vat2jbvw3q.woff?t=1741575060989') format('woff'),
url('//at.alicdn.com/t/c/font_4841764_vat2jbvw3q.ttf?t=1741575060989') format('truetype');
}
@font-face {
font-family: '';
}
.logo {

548
src/fonts/moblie/demo.css Normal file
View File

@@ -0,0 +1,548 @@
/* Logo 字体 */
@font-face {
font-family: 'iconfont logo';
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src:
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix')
format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont')
format('svg');
}
.logo {
font-style: normal;
font-size: 160px;
font-family: 'iconfont logo';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
color: #666;
line-height: 42px;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
position: relative;
z-index: 1;
width: 100px;
height: 40px;
margin-bottom: -1px;
border-bottom: 2px solid transparent;
color: #666;
font-size: 16px;
line-height: 40px;
text-align: center;
cursor: pointer;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
width: 960px;
margin: 0 auto;
padding: 30px 100px;
}
.main .logo {
overflow: hidden;
height: 110px;
margin-top: -50px;
margin-bottom: 30px;
color: #333;
line-height: 1;
text-align: left;
*zoom: 1;
}
.main .logo a {
color: #333;
font-size: 160px;
}
.helps {
margin-top: 40px;
}
.helps pre {
overflow: auto;
margin: 10px 0;
padding: 20px;
border: solid 1px #e7e1cd;
background-color: #fffdef;
}
.icon_lists {
overflow: hidden;
width: 100% !important;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-right: 20px;
margin-bottom: 10px;
list-style: none !important;
text-align: center;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
margin: 10px auto;
color: #333;
font-size: 42px;
line-height: 100px;
-webkit-transition:
font-size 0.25s linear,
width 0.25s linear;
-moz-transition:
font-size 0.25s linear,
width 0.25s linear;
transition:
font-size 0.25s linear,
width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentcolor;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
margin-bottom: 24px;
color: #404040;
font-weight: 500;
line-height: 40px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
clear: both;
margin: 1.6em 0 0.6em;
color: #404040;
font-weight: 500;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
clear: both;
height: 1px;
margin: 16px 0;
border: 0;
background: #e9e9e9;
}
.markdown p {
margin: 1em 0;
}
.markdown > p,
.markdown > blockquote,
.markdown > .highlight,
.markdown > ol,
.markdown > ul {
width: 80%;
}
.markdown ul > li {
list-style: circle;
}
.markdown > ul li,
.markdown blockquote ul > li {
margin-left: 20px;
padding-left: 4px;
}
.markdown > ul li p,
.markdown > ol li p {
margin: 0.6em 0;
}
.markdown ol > li {
list-style: decimal;
}
.markdown > ol li,
.markdown blockquote ol > li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
border-radius: 3px;
background: #eee;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown > table {
width: 95%;
margin-bottom: 24px;
border: 1px solid #e9e9e9;
border-spacing: 0;
border-collapse: collapse;
empty-cells: show;
}
.markdown > table th {
color: #333;
font-weight: 600;
white-space: nowrap;
}
.markdown > table th,
.markdown > table td {
padding: 8px 16px;
border: 1px solid #e9e9e9;
text-align: left;
}
.markdown > table th {
background: #f7f7f7;
}
.markdown blockquote {
margin: 1em 0;
padding-left: 0.8em;
border-left: 4px solid #e9e9e9;
color: #999;
font-size: 90%;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
margin-left: 8px;
opacity: 0;
transition: opacity 0.3s ease;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
display: inline-block;
opacity: 1;
}
.markdown > br,
.markdown > p > br {
clear: both;
}
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: white;
color: #333;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
background-color: #eaffea;
color: #55a532;
}
.hljs-deletion {
background-color: #ffecec;
color: #bd2c00;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*='language-'],
pre[class*='language-'] {
background: none;
color: black;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
line-height: 1.5;
text-align: left;
text-shadow: 0 1px white;
white-space: pre;
word-spacing: normal;
word-wrap: normal;
word-break: normal;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*='language-']::-moz-selection,
pre[class*='language-'] ::-moz-selection,
code[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection {
background: #b3d4fc;
text-shadow: none;
}
pre[class*='language-']::selection,
pre[class*='language-'] ::selection,
code[class*='language-']::selection,
code[class*='language-'] ::selection {
background: #b3d4fc;
text-shadow: none;
}
@media print {
code[class*='language-'],
pre[class*='language-'] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*='language-'] {
overflow: auto;
margin: 0.5em 0;
padding: 1em;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: #f5f2f0;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: 0.7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
background: hsla(0deg, 0%, 100%, 0.5);
color: #9a6e3a;
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #dd4a68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@@ -0,0 +1,75 @@
@font-face {
font-family: mobilefont; /* Project id 4841764 */
src:
url('iconfont.woff2?t=1741575354833') format('woff2'),
url('iconfont.woff?t=1741575354833') format('woff'),
url('iconfont.ttf?t=1741575354833') format('truetype');
}
.mobilefont {
font-style: normal;
font-size: 18px;
font-family: mobilefont !important;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-radiobox::before {
content: '\e75b';
}
.icon-juzhendafen::before {
content: '\e641';
}
.icon-checkbox-checked::before {
content: '\e6c3';
}
.icon-nps::before {
content: '\e6b0';
}
.icon-input::before {
content: '\e6fd';
}
.icon-juzhentiankong::before {
content: '\e62e';
}
.icon-wenjianshangchuan::before {
content: '\e631';
}
.icon-qianming::before {
content: '\e661';
}
.icon-tuwen::before {
content: '\e62c';
}
.icon-juzhenduoxuan::before {
content: '\e818';
}
.icon-juzhendanxuan::before {
content: '\13c7f';
}
.icon-edit2::before {
content: '\e630';
}
.icon-copy::before {
content: '\e632';
}
.icon-delete::before {
content: '\e63f';
}
.icon-sort::before {
content: '\e6a0';
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,114 @@
{
"id": "4841764",
"name": "yl",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "240132",
"name": "radio_box",
"font_class": "radiobox",
"unicode": "e75b",
"unicode_decimal": 59227
},
{
"icon_id": "844024",
"name": "矩阵打分",
"font_class": "juzhendafen",
"unicode": "e641",
"unicode_decimal": 58945
},
{
"icon_id": "1176868",
"name": "checkbox-checked",
"font_class": "checkbox-checked",
"unicode": "e6c3",
"unicode_decimal": 59075
},
{
"icon_id": "8766356",
"name": "nps",
"font_class": "nps",
"unicode": "e6b0",
"unicode_decimal": 59056
},
{
"icon_id": "11215250",
"name": "input",
"font_class": "input",
"unicode": "e6fd",
"unicode_decimal": 59133
},
{
"icon_id": "15969322",
"name": "矩阵填空",
"font_class": "juzhentiankong",
"unicode": "e62e",
"unicode_decimal": 58926
},
{
"icon_id": "15969340",
"name": "文件上传",
"font_class": "wenjianshangchuan",
"unicode": "e631",
"unicode_decimal": 58929
},
{
"icon_id": "17269853",
"name": "签名",
"font_class": "qianming",
"unicode": "e661",
"unicode_decimal": 58977
},
{
"icon_id": "17373529",
"name": "图文",
"font_class": "tuwen",
"unicode": "e62c",
"unicode_decimal": 58924
},
{
"icon_id": "36443758",
"name": "矩阵多选",
"font_class": "juzhenduoxuan",
"unicode": "e818",
"unicode_decimal": 59416
},
{
"icon_id": "38465935",
"name": "矩阵单选",
"font_class": "juzhendanxuan",
"unicode": "13c7f",
"unicode_decimal": 81023
},
{
"icon_id": "1160114",
"name": "edit-2",
"font_class": "edit2",
"unicode": "e630",
"unicode_decimal": 58928
},
{
"icon_id": "1160116",
"name": "copy",
"font_class": "copy",
"unicode": "e632",
"unicode_decimal": 58930
},
{
"icon_id": "1160129",
"name": "delete",
"font_class": "delete",
"unicode": "e63f",
"unicode_decimal": 58943
},
{
"icon_id": "1160227",
"name": "sort",
"font_class": "sort",
"unicode": "e6a0",
"unicode_decimal": 59040
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -9,8 +9,9 @@ export const useCommonStore = defineStore('common', {
state: () => ({
questionsInfo: {
survey: {
id: 8721,
introduction: '<p><span style="color: #e03e2d;">【样本量要求&ge;60】</span></p>',
id: '',
introduction:
'<p>为优化活动服务品质,烦请完成问卷,感谢配合!您的反馈至关重要!(此提示语为默认提示语,您可选择自行输入本问卷的提示语)</p>',
pages: [
[24, 27, 26, 1],
[2, 3, 4, 5, 6, 7],
@@ -18,24 +19,17 @@ export const useCommonStore = defineStore('common', {
[15, 16, 17, 18, 19, 20, 21],
[28, 22]
],
sn: '8YXK4kW5',
sn: '',
status: 0,
title:
'<p><span style="font-size: 24px;"><span style="font-family: \'PingFang SC Regular\';">液态奶产品研究标准化问卷</span><span style="font-family: \'Arial Unicode MS\';">-</span><span style="font-family: \'PingFang SC Regular\';">概念测试(内测标准版)</span></span></p>',
detail_pages: [
[24, 27, 26, 1],
[2, 3, 4, 5, 6, 7],
[8, 9, 10, 11, 12, 13, 14],
[15, 16, 17, 18, 19, 20, 21],
[28, 22]
],
title: '<p>问卷标题</p>',
detail_pages: [],
group_pages: [],
is_one_page_one_question: 0,
last_question_index: 0,
is_three_d_permissions: 0,
is_dept: 1,
last_title: 'Q5',
project_name: '概念诊断问卷示例-0227',
project_name: '',
quota_end_content:
'<p style="text-align:center"><img style="width:220px;margin-top:30px;margin-bottom: 40px;" src="https://cxp-pubcos.yili.com/prod-yls/theme/XxgQ98WN/1693807609602_962_error.png"></p>\n<p style="text-align:center;font-size: 16px;font-weight: 500;padding-bottom: 12px;/* margin-bottom: 10px; */">很抱歉,本次调研不太适合您的情况,感谢您的参与!</p>',
quota_end_url: '',

View File

@@ -0,0 +1,29 @@
export default {
id: '',
question_type: 18,
question_index: 41,
stem: '请上传文件',
title: 'Q14',
options: [],
last_option_index: 0,
config: {
is_required: 1,
quick_type: 0,
is_show: [],
select_random: 0,
min_number: 1,
max_number: 1,
min_size: 0,
max_size: 1,
is_file: 0,
file_type: '0'
},
associate: [],
question_code: '',
logic_config: {
order: 0,
type: 0,
expect: '',
stay_time: ''
}
};

View File

@@ -1,14 +1,14 @@
export default {
// JSON 需要修改
id: '',
question_type: 5,
question_type: '',
question_index: '',
stem: '请完成打分',
stem: '请每行选择一个选项',
title: '',
options: [
[
{
id: 'e46f51b1-bfd8-4d9c-becc-4fb7d175a6f4',
id: '',
is_fixed: 0,
is_other: 0,
is_remove_other: 0,
@@ -25,6 +25,83 @@ export default {
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: []
}
],
[
{
option: '<p style="text-align:center">列标签1</p>',
id: '1049201',
type: 2,
is_other: 0,
is_fixed: 0,
is_remove_other: 0,
created_at: null,
created_user_id: null,
parent_id: null,
option_index: 3,
list_id: 74491,
option_code: '',
option_config: {
title: '',
instructions: [],
price: 0,
gradient: '',
image_url: [],
option_type: 0,
type: 0,
limit_right_content: '',
child_area: null,
binding_goods_id: ''
},
disable_option_update: null,
cascade: []
},
{
option: '<p style="text-align:center">列标签2</p>',
id: '1049202',
type: 2,
is_other: 0,
is_fixed: 0,
is_remove_other: 0,
created_at: null,
created_user_id: null,
parent_id: null,
option_index: 4,
list_id: 74491,
option_code: '',
option_config: {
title: '',
instructions: [],
price: 0,
gradient: '',
image_url: [],
option_type: 0,
type: 0,
limit_right_content: '',
child_area: null,
binding_goods_id: ''
},
disable_option_update: null,
cascade: []
}
]
],

View File

@@ -0,0 +1,54 @@
export default {
id: '',
question_type: 106,
question_index: 0,
stem: '您向朋友或同事推荐我们的可能性多大?',
title: '',
options: [
[
{
id: 'b68d45eb-d833-4b25-b0aa-2fde1310e88d',
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: []
}
]
],
last_option_index: 0,
config: {
is_required: 1,
quick_type: 0,
is_show: [],
version: 2,
prompt_left: '不可能',
prompt_right: '极有可能',
prompt_center: '',
max: 10,
min: 0,
score_interval: 1,
score_type: 0,
score_way: 1,
prompt_score: 2
},
associate: [],
question_code: '',
logic_config: {
order: 0,
type: 0,
expect: '',
stay_time: ''
}
};

View File

@@ -0,0 +1,37 @@
export default {
id: '17852690',
title: 'Q7',
stem: '请留下您的姓名',
other: '',
question_index: 10,
question_type: 22,
config: {
is_required: 1,
select_random: 0,
float_window: 0,
float_window_content: '',
popup_window: 0,
popup_window_content: '',
is_show: [],
quick_type: 0
},
created_at: '2025-03-06T15:51:13',
created_user_id: 1281,
updated_user_id: null,
survey_id: 9482,
logic_config: {
expect: '',
order: 0,
type: 0,
stay_time: ''
},
options: [],
associate: [],
logics_has: null,
last_option_index: 0,
question_code: '',
question_value: '',
question_tag: '',
planet_id: '',
permissions: null
};

View File

@@ -0,0 +1,22 @@
export default {
id: '',
question_type: '',
question_index: 0,
stem: '请认真阅读以下内容',
title: '',
options: [],
last_option_index: 0,
config: {
is_required: 1,
quick_type: 0,
is_show: []
},
associate: [],
question_code: '',
logic_config: {
order: 0,
type: 0,
expect: '',
stay_time: ''
}
};

View File

@@ -4,3 +4,7 @@ export { default as checkbox } from './QuestionJsons/Checkbox.js';
export { default as completion } from './QuestionJsons/Completion.js';
export { default as rate } from './QuestionJsons/Rate.js';
export { default as martrixQuestion } from './QuestionJsons/MartrixQuestion.js';
export { default as fileUpload } from './QuestionJsons/FileUpload.js';
export { default as textWithImages } from './QuestionJsons/TextWithImages.js';
export { default as signQuestion } from './QuestionJsons/SignQuestion.js';
export { default as nps } from './QuestionJsons/NPS.js';

View File

@@ -87,7 +87,11 @@
<template #action="{ element: el }">
<div class="flex slot-actions">
<template v-for="(item, optionIndex) in actionOptions">
<div v-if="item.question_type.includes(el.question_type)" :key="optionIndex" class="flex">
<div
v-if="item.question_type.includes(el.question_type)"
:key="optionIndex"
class="flex"
>
<template v-for="(act, actIndex) in item.actions" :key="actIndex">
<div class="flex align-center action-item" @click="actionEvent(act, el)">
<van-icon :name="act.icon"></van-icon>
@@ -104,8 +108,11 @@
<!-- {{questionInfo.survey.pages.length}}-->
<div v-if="!filterGap">
<paging
v-if="!element.question_type && questionInfo.survey.pages.length > 1" :info="element" :index="index"
:active="pageIsActive(activeIndex, questionInfo.questions, element.page)" @click.stop=""
v-if="!element.question_type && questionInfo.survey.pages.length > 1"
:info="element"
:index="index"
:active="pageIsActive(activeIndex, questionInfo.questions, element.page)"
@click.stop=""
/>
</div>
</template>
@@ -114,7 +121,7 @@
</template>
<script setup>
import { v4 as uuidv4 } from 'uuid';
import { ref, onMounted } from 'vue';
import { ref, onMounted, watch } from 'vue';
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';
import Draggable from './components/Draggable.vue';
@@ -130,6 +137,27 @@ import FileUpload from './components/Questions/FileUpload.vue';
import NPS from '@/views/Design/components/Questions/NPS.vue';
const activeIndex = ref(-1);
// 获取所有的 question 列表内容
const { filterGap, activeId } = defineProps({
filterGap: {
type: Boolean,
required: false,
default: false
},
activeId: {
type: String,
default: ''
}
});
watch(
() => activeId,
(newVal) => {
chooseQuestionId.value = newVal;
}
);
/**
* 工具函数
*/
@@ -176,15 +204,6 @@ function util() {
};
}
// 获取所有的 question 列表内容
const { filterGap } = defineProps({
filterGap: {
type: Boolean,
required: false,
default: false
}
});
const { pageIsActive } = util();
// 获取 Store 实例
const counterStore = useCounterStore();

View File

@@ -13,9 +13,13 @@
<contenteditable v-model="element.stem" :active="active"></contenteditable>
</template>
<template #input>
<template v-for="(item, index) in element.options" :key="index">
<template v-for="(item, optionIndex) in element.options" :key="item.id">
<van-radio-group v-if="element.question_type === 1">
<option-action v-model:data="element.options[index]" :active="active" :question="element">
<option-action
v-model:data="element.options[optionIndex]"
:active="active"
:question="element"
>
<template #item="{ element: it, index: itIndex }">
<van-radio
:key="itIndex"

View File

@@ -19,12 +19,11 @@
</template>
<template #input>
<div v-for="(optionItem, optionItemIndex) in element.options" :key="optionItemIndex">
<div v-for="(item, index) in optionItem" :key="index" @click="chooseOption(item)">
<!-- <div-->
<!-- :contenteditable="item.id === chooseId"-->
<!-- class="van-field"-->
<!-- v-html="item.option"-->
<!-- ></div>-->
<div
v-for="(item, optionIndex) in optionItem"
:key="optionIndex"
@click="chooseOption(item)"
>
<RateCharacter :config="element.config"></RateCharacter>
<div class="tips">
<p>{{ element.config.prompt_left }}</p>

View File

@@ -19,7 +19,11 @@
</template>
<template #input>
<div v-for="(optionItem, optionItemIndex) in element.options" :key="optionItemIndex">
<div v-for="(item, index) in optionItem" :key="index" @click="chooseOption(item)">
<div
v-for="(item, optionIndex) in optionItem"
:key="optionIndex"
@click="chooseOption(item)"
>
<div
:contenteditable="item.id === chooseId"
class="van-field"

View File

@@ -15,7 +15,12 @@ const currentStep = ref(-1);
// 保存当前状态
const saveState = () => {
if (!ctx || !signatureCanvas.value) return;
const imageData = ctx.getImageData(0, 0, signatureCanvas.value.width, signatureCanvas.value.height);
const imageData = ctx.getImageData(
0,
0,
signatureCanvas.value.width,
signatureCanvas.value.height
);
currentStep.value += 1;
// 移除当前步骤之后的所有状态(处理在撤销后又进行了新的绘制的情况)
undoStack.value.splice(currentStep.value);
@@ -58,15 +63,13 @@ onMounted(() => {
// 触摸开始,开始绘制适用于移动设备
signatureCanvas.value?.addEventListener('touchstart', (e) => {
e.preventDefault(); // 防止页面滚动
// 防止页面滚动
e.preventDefault();
isDrawing = true;
const rect = signatureCanvas.value!.getBoundingClientRect();
const touch = e.touches[0];
ctx.beginPath();
ctx.moveTo(
touch.clientX - rect.left,
touch.clientY - rect.top
);
ctx.moveTo(touch.clientX - rect.left, touch.clientY - rect.top);
});
signatureCanvas.value?.addEventListener('touchmove', (e) => {
@@ -74,10 +77,7 @@ onMounted(() => {
if (!isDrawing) return;
const rect = signatureCanvas.value!.getBoundingClientRect();
const touch = e.touches[0];
ctx.lineTo(
touch.clientX - rect.left,
touch.clientY - rect.top
);
ctx.lineTo(touch.clientX - rect.left, touch.clientY - rect.top);
ctx.stroke();
});
@@ -97,19 +97,13 @@ onMounted(() => {
isDrawing = true;
const rect = signatureCanvas.value!.getBoundingClientRect();
ctx.beginPath();
ctx.moveTo(
e.clientX - rect.left,
e.clientY - rect.top
);
ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
});
signatureCanvas.value?.addEventListener('mousemove', (e) => {
if (!isDrawing) return;
const rect = signatureCanvas.value!.getBoundingClientRect();
ctx.lineTo(
e.clientX - rect.left,
e.clientY - rect.top
);
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
ctx.stroke();
});
@@ -162,11 +156,13 @@ const undo = () => {
<van-cell>
<div class="sign-question">
<canvas
ref="signatureCanvas" :width="canvasWidth" :height="canvasHeight"
style="border: 1px solid #ccc; border-radius: 4px;"
ref="signatureCanvas"
:width="canvasWidth"
:height="canvasHeight"
style="border: 1px solid #ccc; border-radius: 4px"
>
</canvas>
<div class="sign-text" :class="{ 'show': showSignText }">
<div class="sign-text" :class="{ show: showSignText }">
<span @click="clearCanvas">清空</span>
<span @click="undo">撤销</span>
<span @click="togglePen">{{ isEraser ? '画笔' : '橡皮擦' }}</span>
@@ -222,4 +218,4 @@ const undo = () => {
}
}
}
</style>
</style>

View File

@@ -8,10 +8,16 @@
<van-cell-group inset class="result-cell">
<div>
<div>请输入问卷标题</div>
<div>
为优化活动服务品质烦请完成问卷感谢配合您的反馈至关重要 (
提示语为默认提示语您可选择自行输入本问卷的提示语
<!--问卷标题-->
<contenteditable v-model="questionInfo.survey.title" :active="true"></contenteditable>
</div>
<div>
<!-- 问卷标注-->
<contenteditable
v-model="questionInfo.survey.introduction"
:active="true"
></contenteditable>
</div>
<button @click="show = true">添加题目</button>
@@ -20,36 +26,43 @@
<div class="ques">
<!-- 题目-->
<Design @get-active-question="getActiveQuestion"></Design>
<van-button @click="show = true">添加题目</van-button>
<Design :active-id="activeId" @get-active-question="getActiveQuestion"></Design>
<!-- <van-button @click="show = true">添加题目</van-button>-->
<!-- 弹出的新增题目弹窗-->
<van-popup v-model:show="show" round position="bottom" :style="{ height: '50%' }">
<van-popup
v-model:show="show"
round
:closeable="true"
position="bottom"
:style="{ maxHeight: '50%' }"
title="添加题目"
>
<van-row class="ques_title">
<van-col :span="6">添加题目</van-col>
<van-col :span="6" :offset="12" @click="show = false">
<van-icon name="close" />
</van-col>
</van-row>
<ul>
<li v-for="item in quesList" :key="item.type" @click="questionEvent(item)">
<div>
<van-icon :name="item.icon" size="20" />
<p>{{ item.name }}</p>
</div>
</li>
</ul>
<van-grid :gutter="10" class="ques_list">
<van-grid-item
v-for="item in quesList"
:key="item.type"
:icon="item.icon"
icon-color="#70b936"
:text="item.name"
@click="questionEvent(item)"
>
<template #icon>
<span class="mobilefont grid-icon" v-html="item.icon"></span>
</template>
</van-grid-item>
</van-grid>
</van-popup>
</div>
<van-cell-group inset class="thanks-cell">
<div>您已完成本次调研感谢您的参与</div>
</van-cell-group>
<!-- 底部功能性按钮 -->
<div class="survey-action">
<div class="survey-action_setting">
<van-icon name="setting" size="18" />
<span>设置</span>
<div class="survey-action_setting" @click="openSettingAction">
<van-icon name="setting" size="18" class="grid-icon" />
<span>投放设置</span>
</div>
<div class="survey-action_btn">
<van-button size="small">预览</van-button>
@@ -58,6 +71,186 @@
</div>
</div>
</div>
<!-- 投放设置-->
<van-action-sheet v-model:show="showSetting" title="">
<template #description>
<div class="flex flex-start">设置</div>
</template>
<van-cell-group :border="false" class="ml10">
<van-cell title="每页一题" :border="false" label-align="left">
<template #right-icon>
<van-switch
v-model="questionInfo.survey.is_page_one_question"
class="option-action-sheet-switch"
size="0.5rem"
:active-value="1"
:inactive-value="0"
></van-switch>
</template>
</van-cell>
<van-divider></van-divider>
<van-cell title="投放数量" :border="false" label-align="left">
<template #right-icon>
<van-switch
v-model="questionInfo.survey.is_publish_number"
class="option-action-sheet-switch"
size="0.5rem"
:active-value="1"
:inactive-value="0"
></van-switch>
</template>
</van-cell>
<van-cell-group
v-if="questionInfo.survey.is_publish_number === 1"
class="child-group"
:border="false"
>
<van-field label="投放数量最大为" input-align="right">
<template #right-icon> </template>
</van-field>
</van-cell-group>
<van-divider></van-divider>
<van-cell title="有效期" :border="false" label-align="left">
<template #right-icon>
<van-switch
v-model="questionInfo.survey.is_time"
class="option-action-sheet-switch"
size="0.5rem"
:active-value="1"
:inactive-value="0"
></van-switch>
</template>
</van-cell>
<!--有效期-->
<van-cell-group v-if="questionInfo.survey.is_time === 1" class="child-group" :border="false">
<van-field
v-model="questionInfo.survey.startTime"
is-link
label="起始时间"
input-align="right"
readonly
@click="showTimePicker('start', questionInfo.survey.startTime)"
>
</van-field>
<van-field
v-model="questionInfo.survey.endTime"
is-link
label="截至时间"
input-align="right"
readonly
@click="showTimePicker('end', questionInfo.survey.endTime)"
>
</van-field>
</van-cell-group>
<van-cell title="断点续答" :border="false" label-align="left">
<template #right-icon>
<van-switch
v-model="questionInfo.survey.is_breakpoint"
class="option-action-sheet-switch"
size="0.5rem"
:active-value="1"
:inactive-value="0"
></van-switch>
</template>
</van-cell>
<van-cell
title="企微身份获取(开启后仅支持私有化企业环境作答)"
:border="false"
label-align="left"
>
<template #right-icon>
<van-switch
v-model="questionInfo.survey.wework_status"
class="option-action-sheet-switch"
size="0.5rem"
:active-value="1"
:inactive-value="0"
></van-switch>
</template>
</van-cell>
<van-divider></van-divider>
<van-cell title="IP答题次数限制" :border="false" label-align="left">
<template #right-icon>
<van-switch
v-model="questionInfo.survey.is_ip_number"
class="option-action-sheet-switch"
size="0.5rem"
:active-value="1"
:inactive-value="0"
></van-switch>
</template>
</van-cell>
<van-cell-group v-if="questionInfo.survey.is_ip_number === 1" class="child-group">
<van-field
v-model="questionInfo.survey.is_number"
label="同一个IP地址只能作答"
:border="false"
input-align="right"
>
<template #right-icon> </template>
</van-field>
</van-cell-group>
<van-cell title="设备答题次数限制" :border="false" label-align="left">
<template #right-icon>
<van-switch
v-model="questionInfo.survey.is_browser_number"
class="option-action-sheet-switch"
size="0.5rem"
:active-value="1"
:inactive-value="0"
></van-switch>
</template>
</van-cell>
<van-cell-group
v-if="questionInfo.survey.is_browser_number === 1"
class="child-group"
:border="false"
>
<van-field
v-model="questionInfo.survey.browser_number"
label="同一个浏览器只能作答"
:border="false"
input-align="right"
>
<template #right-icon> </template>
</van-field>
</van-cell-group>
<van-divider></van-divider>
<van-field
v-model="endText"
label="结束语"
:border="false"
readonly
label-align="left"
input-align="right"
is-link
@click="openEndTextModel"
>
</van-field>
</van-cell-group>
</van-action-sheet>
<!--时间选择-->
<van-popup v-model:show="timePickerModel" position="bottom" round>
<YLPicker
title=""
:showPicker="timePickerModel"
:values="currentDate"
format="YYYY-MM-DD HH:mm:ss"
:units="['年', '月', '日', '时', '分', '秒']"
@cancel="timePickerModel = false"
@confirm="onConfirmDate"
></YLPicker>
</van-popup>
<!-- 结束语选择-->
<van-popup v-model:show="textModel" position="bottom" round>
<van-picker :columns="columns" @confirm="onConfirm"></van-picker>
</van-popup>
</template>
<script setup lang="ts">
@@ -66,8 +259,19 @@ import Design from '@/views/Design/Index.vue';
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';
import { v4 as uuidv4 } from 'uuid';
import { radio, checkbox, completion, rate, martrixQuestion } from '@/utils/importJsons';
import {
radio,
checkbox,
completion,
rate,
martrixQuestion,
fileUpload,
textWithImages,
signQuestion,
nps
} from '@/utils/importJsons';
import { useRoute } from 'vue-router';
import YLPicker from '@/components/YLPicker.vue';
// 获取 Store 实例
const counterStore = useCounterStore();
@@ -78,9 +282,62 @@ const questionInfo = ref(store.questionsInfo.value);
const activeQuestionIndex = ref(-1);
const currentDate = ref();
const currentType = ref();
const route = useRoute();
const surveyTitle = route.meta.title as string;
const show = ref(false);
const textModel = ref(false);
const activeId = ref(0);
const showSetting = ref(false);
const timePickerModel = ref(false);
// picker 结束语选择器
const columns = ref([
{
text: '成功完成',
value: '1'
},
{
text: '题前终止',
value: '2'
},
{
text: '配额超限',
value: '3'
}
]);
const endText = ref('');
const onConfirm = (ev) => {
endText.value = columns.value[ev.selectedValues[0] - 1].text;
textModel.value = false;
};
const openSettingAction = () => {
showSetting.value = true;
};
const showTimePicker = (type, value) => {
timePickerModel.value = true;
currentType.value = type;
currentDate.value = value;
};
const openEndTextModel = () => {
textModel.value = true;
};
const onConfirmDate = (e) => {
if (currentType.value === 'start') {
questionInfo.value.survey.startTime = e;
} else {
questionInfo.value.survey.endTime = e;
}
timePickerModel.value = false;
};
// 获取选中的题目
const getActiveQuestion = (activeQues) => {
@@ -95,21 +352,21 @@ const getActiveQuestion = (activeQues) => {
const quesList = ref([
{
icon: 'location-o',
icon: '&#xe75b;',
name: '单选题',
question_type: '1',
json: radio
},
{
icon: 'like-o',
icon: '&#xe6c3;',
name: '多选题',
question_type: '2',
question_type: 2,
json: checkbox
},
{
icon: 'star-o',
icon: '&#xe6fd;',
name: '填空题',
question_type: '3',
question_type: 4,
json: completion
},
// {
@@ -119,89 +376,75 @@ const quesList = ref([
// json: rate
// },
{
icon: 'cart-o',
icon: '&#xe641;',
name: '数值打分',
question_type: '4',
question_type: 5,
json: rate
},
{
icon: 'comment-o',
icon: '&#x13c7f;',
name: '矩阵单选',
question_type: '8',
question_type: 9,
json: martrixQuestion
},
{
icon: 'bag-o',
icon: '&#xe818;',
name: '矩阵多选',
question_type: '9',
question_type: 10,
json: martrixQuestion
},
{
icon: 'gift-o',
icon: '&#xe62e;',
name: '矩阵填空',
question_type: '10',
question_type: 8,
json: martrixQuestion
},
{
icon: 'bag-o',
icon: '&#xe631;',
name: '文件上传',
question_type: '9'
question_type: 18,
json: fileUpload
},
{
icon: 'bag-o',
icon: '&#xe62c;',
name: '图文说明',
question_type: 6,
json: textWithImages
},
{
icon: '&#xe661;',
name: '签名',
question_type: 22,
json: signQuestion
},
{
icon: '&#xe6b0;',
name: 'NPS',
question_type: '106'
question_type: 106,
json: nps
}
]);
const questionEvent = (item) => {
let questionJson = {};
// switch (item.question_type) {
// // 单选 多选
// case '1':
// case '2':
//
//
// break;
// // 填空
// case '3':
// questionJson = JSON.parse(
// JSON.stringify({
// ...item.json,
// id: uuidv4(),
// question_index: questionInfo.value.survey.last_question_index + 1
// })
// );
// break;
// // 图形打分
// case '4':
// questionJson = JSON.parse(
// JSON.stringify({
// ...item.json,
// id: uuidv4(),
// question_index: questionInfo.value.survey.last_question_index + 1
// })
// );
// break
// }
const id = uuidv4();
questionJson = JSON.parse(
JSON.stringify({
...item.json,
id: uuidv4(),
id,
question_type: Number(item.question_type),
question_index: questionInfo.value.survey.last_question_index + 1,
options:
item.json.options.length > 0
? item.json.options.map((item) => {
return item.map((it) => {
return {
...it,
// 主键生成
id: uuidv4()
};
});
})
return item.map((it) => {
return {
...it,
// 主键生成
id: uuidv4()
};
});
})
: []
})
);
@@ -213,6 +456,8 @@ const questionEvent = (item) => {
}
// 更新题目索引
questionInfo.value.survey.last_question_index += 1;
activeId.value = id;
show.value = false;
};
const init = () => {
@@ -257,34 +502,41 @@ defineExpose({ init });
}
.thanks-cell {
display: flex;
align-items: center;
justify-content: center;
height: 70px;
//display: flex;
//align-items: center;
//justify-content: center;
//height: 70px;
margin: 10px 5px;
}
.grid-icon {
color: #70b936;
font-size: 25px;
}
.ques {
.ques_title {
margin: 20px 0 10px;
margin: 20px 0 10px 20px;
font-weight: bold;
font-size: 16px;
}
ul {
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 10px;
.ques_list {
margin-bottom: 10px;
}
}
li {
width: 80px;
margin: 5px;
padding: 10px;
background-color: #f7fbf5;
font-weight: bold;
font-size: 14px;
}
.child-group {
& ::v-deep .van-field__label {
width: 140px;
color: #bfbfbf;
font-size: 12px;
}
& ::v-deep .van-cell__title {
width: 140px;
color: #bfbfbf;
font-size: 12px;
}
}
@@ -294,9 +546,11 @@ defineExpose({ init });
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
justify-content: space-between;
width: 100%;
height: 50px;
//margin: 0 10px;
background-color: white;
.survey-action_setting {
@@ -304,14 +558,20 @@ defineExpose({ init });
flex-direction: column;
align-items: center;
justify-content: center;
width: 10%;
margin-left: 10px;
//width: 10%;
}
.survey-action_btn {
display: flex;
flex-direction: row;
justify-content: space-around;
width: 60%;
justify-content: space-between;
margin-right: 10px;
& .van-button + .van-button {
margin-left: 10px;
}
}
}
</style>