hookehuyr

fix 依赖文件

Showing 44 changed files with 4525 additions and 4 deletions
......@@ -7,9 +7,42 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AreaPickerField: typeof import('./src/components/AreaPickerField/index.vue')['default']
ButtonField: typeof import('./src/components/ButtonField/index.vue')['default']
CalendarField: typeof import('./src/components/CalendarField/index.vue')['default']
CheckboxField: typeof import('./src/components/CheckboxField/index.vue')['default']
ContactField: typeof import('./src/components/ContactField/index.vue')['default']
DatePickerField: typeof import('./src/components/DatePickerField/index.vue')['default']
DateTimePickerField: typeof import('./src/components/DateTimePickerField/index.vue')['default']
DesField: typeof import('./src/components/DesField/index.vue')['default']
DividerField: typeof import('./src/components/DividerField/index.vue')['default']
EmailField: typeof import('./src/components/EmailField/index.vue')['default']
FileUploaderField: typeof import('./src/components/FileUploaderField/index.vue')['default']
GenderField: typeof import('./src/components/GenderField/index.vue')['default']
IdentityField: typeof import('./src/components/IdentityField/index.vue')['default']
ImageUploaderField: typeof import('./src/components/ImageUploaderField/index.vue')['default']
MarqueeField: typeof import('./src/components/MarqueeField/index.vue')['default']
MultiRuleField: typeof import('./src/components/MultiRuleField/index.vue')['default']
NameField: typeof import('./src/components/NameField/index.vue')['default']
NoteField: typeof import('./src/components/NoteField/index.vue')['default']
NumberField: typeof import('./src/components/NumberField/index.vue')['default']
NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider']
NutDialog: typeof import('@nutui/nutui-taro')['Dialog']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutNoticebar: typeof import('@nutui/nutui-taro')['Noticebar']
PhoneField: typeof import('./src/components/PhoneField/index.vue')['default']
PickerField: typeof import('./src/components/PickerField/index.vue')['default']
RadioField: typeof import('./src/components/RadioField/index.vue')['default']
RatePickerField: typeof import('./src/components/RatePickerField/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
RuleField: typeof import('./src/components/RuleField/index.vue')['default']
SignField: typeof import('./src/components/SignField/index.vue')['default']
TableField: typeof import('./src/components/TableField/index.vue')['default']
Test: typeof import('./src/components/VideoField/test.vue')['default']
TextareaField: typeof import('./src/components/TextareaField/index.vue')['default']
TextField: typeof import('./src/components/TextField/index.vue')['default']
TimePickerField: typeof import('./src/components/TimePickerField/index.vue')['default']
VideoField: typeof import('./src/components/VideoField/index.vue')['default']
}
}
......
......@@ -40,6 +40,7 @@
"@nutui/icons-vue-taro": "^0.0.9",
"@nutui/nutui-taro": "^4.0.0",
"@tarojs/components": "3.6.2",
"@tarojs/extend": "^3.6.2",
"@tarojs/helper": "3.6.2",
"@tarojs/plugin-framework-vue3": "3.6.2",
"@tarojs/plugin-html": "3.6.2",
......
<!--
* @Date: 2022-08-30 14:32:11
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-14 14:37:45
* @FilePath: /data-table/src/components/AreaPickerField/index.vue
* @Description: 省市区选择控件
-->
<template>
<div v-if="HideShow" class="area-picker-field">
<div class="label"><span v-if="item.component_props.required">&nbsp;*</span>{{ item.component_props.label }}</div>
<van-field
name="ignore"
v-model="fieldValue"
is-link
readonly
:required="item.component_props.required"
placeholder="请选择省市区"
:rules="item.rules"
@click="showPicker = true"
:border="show_address ? true : false"
/>
<van-field
v-if="show_address"
name="ignore"
v-model="address"
placeholder="请填写详细地址"
@blur="onBlur"
:rules="item.rules"
:border="false"
/>
<!-- <div
v-if="show_empty"
class="van-field__error-message"
style="padding: 0 1rem 1rem 1rem"
>
地址不能为空
</div> -->
<van-divider />
<van-popup v-model:show="showPicker" position="bottom">
<van-area
v-model="item.city_code"
title=""
:area-list="areaList"
@confirm="onConfirm"
@cancel="showPicker = false"
/>
</van-popup>
</div>
</template>
<script setup>
import { areaList } from "@vant/area-data";
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const emit = defineEmits(["active"]);
const show_empty = ref(false);
const show_address = ref(!props.item.component_props.no_street)
const address = ref("");
const city_code = ref("");
const showPicker = ref(false);
let fieldValue = ref("");
const onConfirm = ({ selectedOptions }) => {
fieldValue.value = selectedOptions.map((option) => option.text).join(" ");
city_code.value = selectedOptions[2]?.value;
props.item.value = {
key: "area_picker",
filed_name: props.item.key,
value: {
address: fieldValue.value + ' ' + address.value,
city_code: city_code.value
},
};
emit("active", props.item.value);
showPicker.value = false;
};
const onBlur = () => {
props.item.value = {
key: "area_picker",
filed_name: props.item.key,
value: {
address: fieldValue.value + ' ' + address.value,
city_code: city_code.value
},
};
emit("active", props.item.value);
}
// 校验模块
const validAreaPicker = () => {
// 必填项
if (props.item.component_props.required && !fieldValue.value) {
show_empty.value = true;
} else if (props.item.component_props.required && !address.value) {
show_empty.value = true;
} else {
show_empty.value = false;
}
return !show_empty.value;
};
defineExpose({ validAreaPicker });
</script>
<style lang="less" scoped>
.area-picker-field {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
}
</style>
<!--
* @Date: 2022-08-29 14:31:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2022-11-22 14:23:49
* @FilePath: /data-table/src/components/DividerField/index.vue
* @Description: 按钮组件
-->
<template>
<div class="button-field-page">
<van-row justify="center" gutter="20">
<van-col v-for="item in props.item.component_props.config" key="index">
<van-button
@click="handleButton(item)"
:icon="iconType(item)"
:color="item.background ? item.background : backgroundColor"
>{{ item.text }}
</van-button>
</van-col>
</van-row>
</div>
<van-overlay :show="show" @click="onClose">
<div class="wrapper">
<div class="block">
<van-image width="100%" fit="cover" :src="qr_url" />
</div>
</div>
</van-overlay>
</template>
<script setup>
import { styleColor } from "@/constant.js";
const props = defineProps({
item: Object,
});
const show = ref(false);
const backgroundColor = styleColor.baseColor;
const iconType = (item) => {
if (item.type === "tel") return "phone-o";
if (item.type === "link") return "link-o";
if (item.type === "qr") return "qr";
};
const qr_url = ref("");
const handleButton = ({ type, content }) => {
if (type === "tel") {
location.href = "tel://" + content;
}
if (type === "link") {
location.href = content;
}
if (type === "qr") {
show.value = true;
qr_url.value = content;
}
};
const onClose = () => {
show.value = false;
};
onMounted(() => {});
</script>
<style lang="less" scoped>
.button-field-page {
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.block {
width: 10rem;
height: 10rem;
background-color: #fff;
}
</style>
<!--
* @Date: 2022-09-14 11:00:01
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-10 10:06:27
* @FilePath: /data-table/src/components/CalendarField/index.vue
* @Description: 日历选择控件
-->
<template>
<div class="calendar-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<van-field
v-model="item.value"
is-link
readonly
:name="item.key"
:required="item.component_props.required"
:placeholder="item.component_props.placeholder"
:rules="item.rules"
@click="show = true"
/>
<van-calendar
v-model:show="show"
:type="item.component_props.type"
:max-range="item.component_props.max_range"
:min-date="item.component_props.min_date"
:max-date="item.component_props.max_date"
:formatter="formatter"
first-day-of-week="1"
@month-show="onMonthShow"
@confirm="onConfirm"
allow-same-day
/>
</div>
</template>
<script setup>
const props = defineProps({
item: Object,
});
const show = ref(false);
const formatDate = (date) =>
`${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
const onConfirm = (value) => {
console.warn(props.item.component_props.type);
show.value = false;
if (props.item.component_props.type === "range") {
// 日期区间
const [start, end] = value;
props.item.value = `${formatDate(start)} ~ ${formatDate(end)}`;
} else if (props.item.component_props.type === "multiple") {
// 多个日期
const arr = [];
value.forEach((element) => {
arr.push(formatDate(element));
});
props.item.value = arr.join(",");
} else {
props.item.value = formatDate(value);
}
};
// 每一格内容格式化
const formatter = (day) => {
const month = day.date.getMonth() + 1;
const date = day.date.getDate();
const year = day.date.getFullYear();
if (month === 5) {
if (date === 1) {
day.topInfo = "劳动节";
} else if (date === 4) {
day.topInfo = "青年节";
} else if (date === 11) {
day.text = "今天";
}
}
if (month === 10) {
if (date === 1) {
day.topInfo = "国庆节";
day.type = "disabled";
}
}
if (day.type === "start") {
day.bottomInfo = "开始";
} else if (day.type === "end") {
day.bottomInfo = "结束";
}
return day;
};
const onMonthShow = ({ date, title }) => {
// console.warn(date);
// console.warn(title);
if (title === "2022年12月") {
}
};
</script>
<style lang="less" scoped>
.calendar-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
}
</style>
<!--
* @Date: 2022-08-30 11:34:19
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-10 10:06:43
* @FilePath: /data-table/src/components/CheckboxField/index.vue
* @Description: 多项选择控件
-->
<template>
<div v-if="HideShow" class="checkbox-field-page">
<div class="label">
<span v-if="item.component_props.required" style="color: red">&nbsp;*</span>
{{ item.component_props.label }}
<span v-if="item.component_props.max" style="color: gray">
(最多可选数:&nbsp;{{ item.component_props.max }})
</span>
</div>
<div v-if="item.component_props.note" class="note" v-html="item.component_props.note" />
<van-field :rules="item.rules" :border="false">
<template #input>
<van-checkbox-group v-model="checkbox_value" :direction="item.component_props.direction"
:max="item.component_props.max" style="width: 100%">
<div v-for="x in item.component_props.options" :key="x.title" class="checkbox-wrapper">
<van-checkbox @click="onClick(x)" :name="x.title" icon-size="1rem" shape="square"
:checked-color="themeVars.radioColor" style="margin-bottom: 0.25rem">{{ x.title }}</van-checkbox>
<van-field v-if="checkbox_value.includes(x.value) && x.is_input" @blur="onBlur(x)" v-model="x.affix"
label=" " label-width="5px" :placeholder="x.input_placeholder" :rules="x.input_required ? rules : ''"
:required="x.input_required" class="affix-input" />
</div>
</van-checkbox-group>
</template>
</van-field>
</div>
</template>
<script setup>
import { styleColor } from "@/constant.js";
const props = defineProps({
item: Object,
});
// TAG: 自定义主题颜色
const themeVars = {
radioColor: styleColor.baseColor,
};
onMounted(() => {
// 默认值为数组
props.item.value = props.item.component_props.default;
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
// TODO: 等待数据结构更新,看看怎么判断必填
// 校验函数返回 true 表示校验通过,false 表示不通过
const validator = (val) => {
if (!val) {
return false;
} else {
return true;
}
};
// 错误提示文案
const validatorMessage = (val, rule) => {
if (!val) {
return "补充信息不能为空";
}
};
const rules = [{ validator, message: validatorMessage }];
const emit = defineEmits(["active"]);
const checkbox_value = ref(props.item.component_props.default);
const affix_value = ref({});
const onClick = (item) => {
item.checked = !item.checked;
handleEmit(item)
}
const onBlur = (item) => {
handleEmit(item)
}
const handleEmit = (item) => {
// 选中状态添加属性
if (item.checked) {
affix_value.value[item.value] = item.affix ? `${item.title}: ${item.affix}` : '';
} else {
// 为选中删除属性
delete affix_value.value[item.value]
}
// 发送自定义数据结构
props.item.value = { key: props.item.key, value: checkbox_value.value, affix: affix_value.value, type: "checkbox" };
emit("active", props.item.value);
}
onMounted(() => {
// 发送自定义数据结构
props.item.value = { key: props.item.key, value: checkbox_value.value, affix: affix_value.value, type: "checkbox" };
emit("active", props.item.value);
})
</script>
<style lang="less" scoped>
.checkbox-field-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
}
.note {
font-size: 0.9rem;
margin-left: 1rem;
color: gray;
padding-bottom: 0.5rem;
white-space: pre-wrap;
}
.checkbox-wrapper {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
margin-bottom: 0.25rem;
}
.affix-input {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
margin-top: 0.5rem;
margin-bottom: 0.25rem;
}
}
:deep(.van-checkbox) {
// border: 1px solid #eaeaea;
// border-radius: 0.25rem;
// padding: 0.25rem 0.5rem;
}
</style>
<!--
* @Date: 2022-11-23 14:41:53
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-01-06 19:04:00
* @FilePath: /data-table/src/components/ContactField/index.vue
* @Description: 联系我们控件
-->
<template>
<div v-if="HideShow" class="contact-field-page">
<van-popover v-model:show="showPopover" placement="left">
<div class="contact-content">
<div class="text-tel">电话联系</div>
<div>
<a
:style="{ color: styleColor.baseColor }"
:href="`tel:${item.component_props.tel}`"
>
{{ item.component_props.tel }}
</a>
</div>
<div v-if="item.component_props.image_url">
<div class="text-qr_code">微信联系</div>
<van-image width="100" height="100" :src="item.component_props.image_url" />
</div>
</div>
<template #reference>
<div class="wrapper">
<van-icon name="phone-o" size="2rem" :color="styleColor.baseColor" />
</div>
</template>
</van-popover>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { styleColor } from "@/constant.js";
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const showPopover = ref(false);
</script>
<style lang="less" scoped>
.contact-field-page {
position: fixed;
bottom: 10rem;
right: 0.5rem;
// height: 100%;
z-index: 9;
.wrapper {
background: white;
width: 3rem;
height: 3rem;
border-radius: 50%;
text-align: center;
box-shadow: 0rem -0.17rem 0.67rem 0.08rem rgba(0, 0, 0, 0.05);
:deep(.van-icon) {
line-height: 1.5;
}
}
}
.contact-content {
padding: 1rem 0.85rem;
text-align: center;
.text-tel {
color: gray;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.text-qr_code {
color: gray;
font-size: 0.9rem;
margin-bottom: 0.5rem;
margin-top: 0.5rem;
}
}
</style>
<!--
* @Date: 2022-08-31 11:45:30
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-10 10:07:31
* @FilePath: /data-table/src/components/DatePickerField/index.vue
* @Description: 日期选择组件
-->
<template>
<div v-if="HideShow" class="date-picker-field">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<van-field
v-model="item.value"
is-link
readonly
:name="item.key"
:required="item.component_props.required"
:disabled="item.component_props.readonly"
:placeholder="item.component_props.placeholder ? item.component_props.placeholder : '请选择日期'"
:rules="rules"
@click="onTap"
:border="false"
/>
<van-popup v-model:show="showPicker" position="bottom">
<van-date-picker
v-model="currentDate"
title="日期选择"
:min-date="minDate"
:max-date="maxDate"
:columns-type="columns_type"
@confirm="onConfirm"
@cancel="showPicker = false"
/>
</van-popup>
</div>
</template>
<script setup>
import dayjs from "dayjs";
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const showPicker = ref(false);
const currentDate = ref([]);
const readonly = props.item.component_props.readonly;
const onTap = () => {
if (readonly) return false; // 如果为只读,不能设置
showPicker.value = true
}
const onConfirm = ({ selectedValues, selectedOptions }) => {
props.item.value = selectedValues.join("-");
showPicker.value = false;
};
const columns_type = ref([]);
const date_format = props.item.component_props.data_dateformat; // YYYY-MM=年月,YYYY-MM-DD=年月日
// 数字前面补位
const formatZero = (num, len) => {
if (String(num).length > len) {
return num;
}
return (Array(len).join(0) + num).slice(-len)
}
const minDate = ref()
const maxDate = ref()
onMounted(() => {
// 根据默认值时间调整显示
currentDate.value = props.item.component_props.default ? props.item.component_props.default.split("-") : props.item.value.split("-");
let Year = '';
let Month = '';
let Day = '';
if (!props.item.component_props.default) {
Year = String(dayjs().year());
Month = formatZero(dayjs().month(), 2);
Day = formatZero(dayjs().date(), 2);
} else {
Year = currentDate.value[0];
Month = formatZero(currentDate.value[1], 2);
Day = formatZero(currentDate.value[2], 2);
}
switch (date_format) {
case "YYYY-MM":
columns_type.value = ['year', 'month']
// 设置默认值
currentDate.value = [Year, Month];
break;
case "YYYY-MM-DD":
columns_type.value = ['year', 'month', 'day']
// 设置默认值
currentDate.value = [Year, Month, Day];
break;
}
// 设置默认最大最小日期
if (data_minvalue.length) {
const min = data_minvalue.split("-")
minDate.value = new Date(+min[0], +min[1] - 1, +min[2])
}
if (data_maxvalue.length) {
const max = data_maxvalue.split("-")
maxDate.value = new Date(+max[0], +max[1] - 1, +max[2])
}
});
const required = props.item.component_props.required;
const data_minvalue = props.item.component_props.data_minvalue;
const data_maxvalue = props.item.component_props.data_maxvalue;
const validator = (val) => {
if (required && !val) {
return false;
} else if (val && data_minvalue && val < data_minvalue) {
return false;
} else if (val && data_maxvalue && val > data_maxvalue) {
return false;
} else {
return true;
}
};
// 错误提示文案
const validatorMessage = (val, rule) => {
if (required && !val) {
return "必填项不能为空";
} else if (val && data_minvalue && val < data_minvalue) {
return "最小可选:" + data_minvalue;
} else if (val && data_maxvalue && val > data_maxvalue) {
return "最大可选:" + data_maxvalue;
}
};
const rules = [{ validator, message: validatorMessage }];
</script>
<style lang="less" scoped>
.date-picker-field {
margin: 1rem;
.label {
// padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
:deep(.van-icon) { // 处理正式服务器上箭头上下位移问题
font-size: var(--van-cell-icon-size);
line-height: var(--van-cell-line-height);
}
}
:deep(.van-cell--clickable) {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
margin-top: 0.5rem;
input {
color: #323233;
}
}
</style>
<!--
* @Date: 2022-09-08 15:02:45
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-07 16:46:30
* @FilePath: /data-table/src/components/DateTimePickerField/index.vue
* @Description: 日期时间选择器
-->
<template>
<div v-if="HideShow" class="datetime-picker">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<van-field
v-model="item.value"
is-link
readonly
:name="item.key"
:required="item.component_props.required"
:disabled="item.component_props.readonly"
:placeholder="item.component_props.placeholder ? item.component_props.placeholder : '请选择日期时间'"
:rules="rules"
@click="onTap"
:border="false"
/>
<van-popup v-model:show="showPicker" position="bottom">
<van-picker-group
title="请选择日期时间"
:tabs="['选择日期', '选择时间']"
@confirm="onConfirm"
@cancel="onCancel"
>
<van-date-picker v-model="currentDate" :min-date="minDate" :max-date="maxDate" :columns-type="columns_date_type" />
<van-time-picker v-model="currentTime" :columns-type="columns_time_type" />
</van-picker-group>
</van-popup>
</div>
</template>
<script setup>
import { showToast } from "vant";
import dayjs from "dayjs";
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const showPicker = ref(false);
const readonly = props.item.component_props.readonly;
const onTap = () => {
if (readonly) return false; // 如果为只读,不能设置
showPicker.value = true
}
const currentDate = ref([]);
const currentTime = ref([]);
const onConfirm = () => {
props.item.value = `${currentDate.value.join("-")} ${currentTime.value.join(":")}`;
showPicker.value = false;
};
const onCancel = () => {
showPicker.value = false;
};
const columns_date_type = ref([]);
const columns_time_type = ref([]);
const date_format = props.item.component_props.data_dateformat;
// 数字前面补位
const formatZero = (num, len) => {
if (String(num).length > len) {
return num;
}
return (Array(len).join(0) + num).slice(-len)
}
const minDate = ref()
const maxDate = ref()
onMounted(() => {
// 根据默认值时间调整显示
const datetime = props.item.component_props.default ? props.item.component_props.default.split(" ") : props.item.value.split(" ");
currentDate.value = datetime[0]?.split("-");
currentTime.value = datetime[1]?.split(":");
// YYYY=年,YYYY-MM=年月,YYYY-MM-DD=年月日,YYYY-MM-DD HH=年月日时,YYYY-MM-DD HH:mm=年月日时分,YYYY-MM-DD HH:mm:ss=年月日时分秒
let Year = '';
let Month = '';
let Day = '';
if (!props.item.component_props.default) {
Year = String(dayjs().year());
Month = formatZero(dayjs().month(), 2);
Day = formatZero(dayjs().date(), 2);
} else {
Year = currentDate.value[0];
Month = formatZero(currentDate.value[1], 2);
Day = formatZero(currentDate.value[2], 2);
}
let Hour = ''
let Minute = ''
let Second = ''
if (!props.item.component_props.default) {
Hour = String(dayjs().hour());
Minute = String(dayjs().minute());
Second = String(dayjs().second());
} else {
Hour = currentTime.value[0];
Minute = currentTime.value[1];
Second = currentTime.value[2];
}
switch (date_format) {
case "YYYY":
columns_date_type.value = ['year']
// 设置默认值
currentDate.value = [Year];
break;
case "YYYY-MM":
columns_date_type.value = ['year', 'month']
// 设置默认值
currentDate.value = [Year, Month];
break;
case "YYYY-MM-DD":
columns_date_type.value = ['year', 'month', 'day']
// 设置默认值
currentDate.value = [Year, Month, Day];
break;
case "YYYY-MM-DD HH":
columns_date_type.value = ['year', 'month', 'day']
columns_time_type.value = ['hour']
// 设置默认值
currentDate.value = [Year, Month, Day];
currentTime.value = [Hour];
break;
case "YYYY-MM-DD HH:mm":
columns_date_type.value = ['year', 'month', 'day']
columns_time_type.value = ['hour', 'minute']
// 设置默认值
currentDate.value = [Year, Month, Day];
currentTime.value = [Hour, Minute];
break;
case "YYYY-MM-DD HH:mm:ss":
columns_date_type.value = ['year', 'month', 'day']
columns_time_type.value = ['hour', 'minute', 'second']
// 设置默认值
currentDate.value = [Year, Month, Day];
currentTime.value = [Hour, Minute, Second];
break;
}
// 设置默认最大最小日期
if (data_minvalue.split(" ")[0].length) {
const min = data_minvalue.split(" ")[0].split("-")
minDate.value = new Date(+min[0], +min[1] - 1, +min[2])
}
if (data_maxvalue.split(" ")[0].length) {
const max = data_maxvalue.split(" ")[0].split("-")
maxDate.value = new Date(+max[0], +max[1] - 1, +max[2])
}
});
const required = props.item.component_props.required;
const data_minvalue = props.item.component_props.data_minvalue;
const data_maxvalue = props.item.component_props.data_maxvalue;
const validator = (val) => {
if (required && !val) {
return false;
} else if (val && data_minvalue && val < data_minvalue) {
return false;
} else if (val && data_maxvalue && val > data_maxvalue) {
return false;
} else {
return true;
}
};
// 错误提示文案
const validatorMessage = (val, rule) => {
if (required && !val) {
return "必填项不能为空";
} else if (val && data_minvalue && val < data_minvalue) {
return "最小可选:" + data_minvalue;
} else if (val && data_maxvalue && val > data_maxvalue) {
return "最大可选:" + data_maxvalue;
}
};
const rules = [{ validator, message: validatorMessage }];
</script>
<style lang="less" scoped>
.datetime-picker {
margin: 1rem;
.label {
// padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
:deep(.van-icon) { // 处理正式服务器上箭头上下位移问题
font-size: var(--van-cell-icon-size);
line-height: var(--van-cell-line-height);
}
}
:deep(.van-cell--clickable) {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
margin-top: 0.5rem;
input {
color: #323233;
}
}
</style>
<!--
* @Date: 2022-08-29 14:31:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2022-12-08 14:52:48
* @FilePath: /data-table/src/components/DesField/index.vue
* @Description: 描述文本控件
-->
<template>
<div class="des-field-page">
<div class="label">{{ item.component_props.label }}</div>
<van-field
v-model="item.component_props.desc"
name="ignore"
:readonly="true"
:border="false"
/>
</div>
<van-divider />
</template>
<script setup>
const props = defineProps({
item: Object,
});
</script>
<style lang="less" scoped>
.des-field-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
}
</style>
<!--
* @Date: 2022-08-29 14:31:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2022-11-22 14:23:49
* @FilePath: /data-table/src/components/DividerField/index.vue
* @Description: 分隔线组件
-->
<template>
<div class="divider-field-page">
<van-divider :style="styleObj">
{{ item.component_props.content }}
</van-divider>
</div>
</template>
<script setup>
const props = defineProps({
item: Object,
});
const styleObj = ref({});
onMounted(() => {
styleObj.value = {
color: props.item.component_props.color,
borderColor: props.item.component_props.color,
padding: props.item.component_props.padding,
};
});
</script>
<style lang="less" scoped>
.divider-field-page {
}
</style>
<!--
* @Date: 2022-08-29 14:31:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-01-18 15:58:11
* @FilePath: /data-table/src/components/EmailField/index.vue
* @Description: 邮箱输入框
-->
<template>
<div v-if="HideShow" class="text-field-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<van-field
v-model="item.value"
:name="item.name"
type="email"
:placeholder="item.component_props.placeholder"
:rules="rules"
:required="item.component_props.required"
:disabled="item.component_props.disabled"
:readonly="item.component_props.readonly"
clearable
/>
</div>
</template>
<script setup>
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const required = props.item.component_props.required;
const validator = (val) => {
if (required && !val) {
return false;
} else if (val && !/^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-])+/.test(val)) {
return false;
} else {
return true;
}
};
// 错误提示文案
const validatorMessage = (val, rule) => {
if (required && !val) {
return "必填项不能为空";
} else if (val && !/^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-])+/.test(val)) { // 小于最小值
return "请输入正确邮箱";
}
};
const rules = [{ validator, message: validatorMessage }];
</script>
<style lang="less" scoped>
.text-field-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
}
:deep(.van-field__body) {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
</style>
<!--
* @Date: 2022-08-31 16:16:49
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-10 11:17:21
* @FilePath: /data-table/src/components/FileUploaderField/index.vue
* @Description: 文件上传控件
-->
<template>
<div v-if="HideShow" class="file-uploader-field">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<div
v-if="item.component_props.note"
v-html="item.component_props.note"
style="font-size: 0.9rem; margin-left: 1rem; color: gray; padding-bottom: 0.5rem; padding-top: 0.25rem; white-space: pre-wrap;"
/>
<div>
<p
v-for="(file, index) in fileList"
:key="index"
style="padding-left: 1rem; margin-bottom: 0.5rem"
>
<p style="font-size: 1rem; word-break: break-all; margin-right: 0.75rem;">
<span>{{ index + 1 }}.&nbsp;{{ file.filename }}&nbsp;&nbsp;{{ (file.size / 1024 / 1024).toFixed(2) }}MB</span>
&nbsp;&nbsp;
<span style="color: #e32525; font-size: 0.85rem" @click="beforeDelete(file)">移除</span>
</p>
</p>
</div>
<div style="padding: 1rem">
<van-uploader
:name="item.name"
upload-icon="add"
accept="*"
:before-read="beforeRead"
:after-read="afterRead"
:before-delete="beforeDelete"
:multiple="item.component_props.max_size > 1"
>
<van-button icon="plus" type="primary">上传文件</van-button>
</van-uploader>
</div>
<!-- <div class="type-text">上传格式:{{ type_text }}</div> -->
<div
v-if="show_empty"
class="van-field__error-message"
style="padding: 0 1rem 1rem 1rem"
>
文件上传不能为空
</div>
<van-divider />
</div>
<van-overlay :show="loading">
<div class="wrapper" @click.stop>
<van-loading vertical color="#FFFFFF">上传中...</van-loading>
</div>
</van-overlay>
</template>
<script setup>
/**
* 文件上传
* @param name[String] 组件名称
* @param file_type[Array] 文件上传类型
* @param multiple[Boolean] 文件多选
*/
import { showSuccessToast, showFailToast, showToast } from "vant";
import _ from "lodash";
import { v4 as uuidv4 } from "uuid";
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from "@/api/common";
import BMF from "browser-md5-file";
import { useRoute } from "vue-router";
import axios from "axios";
import { getEtag } from "@/utils/qetag.js"; // 生成hash值
const $route = useRoute();
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const emit = defineEmits(["active"]);
const show_empty = ref(false);
// 文件类型中文页面显示
const type_text = computed(() => {
return props.item.component_props.file_type;
});
// 上传文件集合
const fileList = ref([
// { url: "https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg" },
// Uploader 根据文件后缀来判断是否为文件文件
// 如果文件 URL 中不包含类型信息,可以添加 isImage 标记来声明
// { url: 'https://cloud-image', isImage: true },
]);
// 上传前置处理
const beforeRead = (file) => {
// TODO: 需要file_type集合
// 类型限制
// const file_types = _.map(
// props.item.component_props.file_type.split("/"),
// (item) => `video/${item}`
// );
let flag = true;
if (file.length + 1 > props.item.component_props.max_count) {
// 数量限制
flag = false;
showToast(`最大上传数量为${props.item.component_props.max_count}个`);
}
if (fileList.value.length + 1 > props.item.component_props.max_count) {
// 数量限制
flag = false;
showToast(`最大上传数量为${props.item.component_props.max_count}个`);
}
if ((file.size / 1024 / 1024).toFixed(2) > props.item.component_props.max_size) {
// 体积限制
flag = false;
showToast(
`最大文件体积为${props.item.component_props.max_size}MB`
);
}
// if (_.isArray(file)) {
// // 多张文件
// const types = _.difference(_.uniq(_.map(file, (item) => item.type)), file_types); // 数组返回不能上传的类型
// if (types.length) {
// flag = false;
// showFailToast("请上传指定格式文件");
// }
// } else {
// if (!_.includes(file_types, file.type)) {
// showFailToast("请上传指定格式文件");
// flag = false;
// }
// }
return flag;
};
// 文件读取完成后的回调函数
const afterRead = async (files) => {
if (Array.isArray(files)) {
// 多张文件上传files是一个数组
muliUpload(files);
} else {
const imgUrl = await handleUpload(files);
// 上传失败提示
if (!imgUrl.src) {
files.status = "failed";
files.message = "上传失败";
loading.value = false;
} else {
files.status = "";
files.message = "";
fileList.value.push({
// meta_id: imgUrl.meta_id,
name: files.file.name,
url: imgUrl.src,
size: files.file.size
// isImage: true,
});
loading.value = false;
}
}
// 过滤非包含URL的文件
fileList.value = fileList.value.filter((item) => {
if (item.url) return item;
});
props.item.value = {
key: "file_uploader",
filed_name: props.item.key,
// value: fileList.value.map((item) => item.url),
value: fileList.value,
};
show_empty.value = false;
// 完整数据回调到表单上
emit("active", props.item.value);
};
// 文件删除前的回调函数
const beforeDelete = (files) => {
fileList.value = fileList.value.filter((item) => {
if (item.url !== files.url) return item;
});
props.item.value = {
key: "file_uploader",
filed_name: props.item.key,
// value: fileList.value.map((item) => item.url),
value: fileList.value,
};
// 完整数据回调到表单上
emit("active", props.item.value);
};
/********** 上传七牛云获取文件地址 ***********/
const loading = ref(false);
const formCode = $route.query.code; // 表单code
// 上传文件返回文件URL
const handleUpload = async (files) => {
loading.value = true;
// 获取HASH值
// const hash = getEtag(files.content);
return new Promise((resolve, reject) => {
// 获取MD5值
const bmf = new BMF();
bmf.md5(
files.file,
async (err, md5) => {
if (err) {
console.log(err);
reject(err);
}
// 获取七牛token
const filename = files.file.name; // 真实文件名
const getToken = await qiniuTokenAPI({
name: filename,
hash: md5,
});
// 文件上传七牛云
let imgUrl = "";
// 第一次上传
if (getToken.token) {
files.status = "uploading";
files.message = "上传中...";
// 返回数据库真实文件地址
imgUrl = await uploadQiniu(files.file, getToken.token, filename, md5);
}
// 重复上传
if (getToken.data) {
imgUrl = getToken.data;
}
resolve(imgUrl);
},
(process) => {
//计算进度
}
);
});
};
// 多选文件上传遍历
var muliUpload = async (files) => {
for (let item of files) {
const res = await handleUpload(item);
// 上传失败提示
if (!res.src) {
item.status = "failed";
item.message = "上传失败";
loading.value = false;
} else {
item.status = "";
item.message = "";
fileList.value.push({
// meta_id: res.meta_id,
name: item.file.name,
url: res.src,
size: files.file.size
// isImage: true,
});
loading.value = false;
}
}
};
// 生成数据库真实文件地址
const uploadQiniu = async (file, token, name, md5) => {
let suffix = /\.[^\.]+$/.exec(name); // 获取后缀
// let affix = uuidv4();
let fileName = `uploadForm/${formCode}/${md5}${suffix}`;
let formData = new FormData();
formData.append("file", file); // 通过append向form对象添加数据
formData.append("token", token);
formData.append("key", fileName);
let config = {
headers: { "Content-Type": "multipart/form-data" },
};
// 自拍文件上传七牛服务器
let qiniuUploadUrl;
if (window.location.protocol === 'https:') {
qiniuUploadUrl = 'https://up.qbox.me';
} else {
qiniuUploadUrl = 'http://upload.qiniu.com';
}
const { filekey, hash, image_info } = await qiniuUploadAPI(
qiniuUploadUrl,
formData,
config
);
if (filekey) {
// 保存文件
const { data } = await saveFileAPI({
name,
filekey,
hash: md5,
// format: image_info.format,
// height: image_info.height,
// width: image_info.width,
});
return data;
}
};
/****************** END *******************/
// 校验模块
const validFileUploader = () => {
// 必填项 未上传文件
if (props.item.component_props.required && !fileList.value.length) {
show_empty.value = true;
} else {
show_empty.value = false;
}
return !show_empty.value;
};
defineExpose({ validFileUploader });
</script>
<style lang="less" scoped>
.file-uploader-field {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
.type-text {
font-size: 0.9rem;
margin-left: 1rem;
padding-bottom: 1rem;
color: gray;
}
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.block {
width: 120px;
height: 120px;
background-color: #fff;
}
</style>
<!--
* @Date: 2022-08-30 11:34:19
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-01 11:18:31
* @FilePath: /data-table/src/components/GenderField/index.vue
* @Description: 性别选择控件
-->
<template>
<div v-if="HideShow" class="gender-field-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<div v-if="item.component_props.note" class="note" v-html="item.component_props.note" />
<van-field
:name="item.name"
:rules="item.rules"
:required="item.component_props.required"
:disabled="item.component_props.disabled"
>
<template #input>
<van-radio-group v-model="item.value" direction="horizontal">
<van-radio name="男" :checked-color="themeVars.radioColor">男</van-radio>
<van-radio name="女" :checked-color="themeVars.radioColor">女</van-radio>
</van-radio-group>
</template>
</van-field>
</div>
</template>
<script setup>
import { styleColor } from "@/constant.js";
const props = defineProps({
item: Object,
});
// TAG: 自定义主题颜色
const themeVars = {
radioColor: styleColor.baseColor,
};
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
onMounted(() => {
props.item.value = props.item.component_props.default;
})
</script>
<style lang="less" scoped>
.gender-field-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
.note {
font-size: 0.9rem;
margin-left: 1rem;
color: gray;
padding-bottom: 0.5rem;
white-space: pre-wrap;
}
}
</style>
<!--
* @Date: 2022-09-14 14:44:30
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-01 15:07:15
* @FilePath: /data-table/src/components/IdentityField/index.vue
* @Description: 身份证输入控件
-->
<template>
<div v-if="HideShow" class="identity-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<!-- <div v-if="item.component_props.readonly" style="padding: 0.5rem 1rem;">{{ item.value }}</div> -->
<van-field
v-model="item.value"
:id="item.name"
:name="item.name"
:placeholder="item.component_props.placeholder"
:rules="rules"
:required="item.component_props.required"
:disabled="item.component_props.readonly"
readonly
@touchstart.stop="openKeyboard($event)"
:border="false"
>
</van-field>
<!-- <div v-if="gender" class="gender"><span>性别:</span>{{ gender }}</div> -->
<van-number-keyboard
v-model="item.value"
:show="show"
extra-key="X"
close-button-text="完成"
@blur="blurKeyboard()"
@input="onInput"
@delete="onDelete"
safe-area-inset-bottom
/>
</div>
</template>
<script setup>
import $ from "jquery";
import { storeToRefs, mainStore } from "@/utils/generatePackage";
import { showSuccessToast, showFailToast } from "vant";
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const show = ref(false);
let content = "";
const store = mainStore();
const { fieldName } = storeToRefs(store);
// 监听字段变化
watch(
() => fieldName.value,
(v) => {
// 如果不是点击本输入框
if (v !== props.item.name) {
// 还原border颜色
$(`#${props.item.name}`).parent().css("border-color", "#eaeaea");
show.value = false;
document.getElementById("app").style.paddingBottom = "0";
}
}
);
const readonly = props.item.component_props.readonly;
const openKeyboard = (e) => {
if (readonly) return false; // 如果为只读,不能设置
// // 键盘上移动
// const target_to_view_height = window.innerHeight - e.target.getBoundingClientRect().y; // 元素到适口高度
// const target_top = document.body.scrollHeight - $(e.target).offset().top; // 元素到正文高度
// let scroll_height = "";
// console.warn(target_top);
// if (target_top < 250) {
// document.getElementById("app").style.paddingBottom = "250px";
// window.scrollTo(0, $("#app").height());
// } else {
// // 向上滚动位置
// document.documentElement.scrollTop = (target_top > 250 ? 0 : target_top) + 250;
// }
// 键盘上移动
const target_to_view_height =
window.innerHeight - e.target.getBoundingClientRect().bottom; // 元素到适口高度
const target_top = document.body.scrollHeight - $(e.target).offset().top; // 元素到正文高度
let scroll_height = "";
if (target_to_view_height <= 250) {
document.getElementById("app").style.paddingBottom = "250px";
// 向上滚动位置
document.documentElement.scrollTop = $(e.target).offset().top - 244;
}
// 选中添加border颜色
content = $(e.target).parent();
// TAG: 自定义主题颜色
content.css("border-color", "#c2915f");
setTimeout(() => {
show.value = true;
}, 300);
// 记录点击field名
store.changeFieldName(props.item.name);
};
const blurKeyboard = () => {
show.value = false;
document.getElementById("app").style.paddingBottom = "0";
// 还原border颜色
content.css("border-color", "#eaeaea");
// 键盘失焦检查输入和添加性别显示
const input_val = props.item.value;
if (required && !input_val) {
showFailToast("身份证号码不能为空");
} else if (input_val && !/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(input_val)) {
showFailToast("请输入正确身份证号码");
} else {
// gender.value = getGenderByIdNumber(input_val)
}
};
// 校验函数返回 true 表示校验通过,false 表示不通过
// 身份证号码为15位或者18位,15位时全为数字,18位前17位为数字,最后一位是校验位,可能为数字或字符X
const required = props.item.component_props.required;
const validator = (val) => {
if (required && !val) {
return false;
} else if (val && !/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(val)) {
return false;
} else {
return true;
}
};
// 错误提示文案
const validatorMessage = (val, rule) => {
if (required && !val) {
return "身份证号码不能为空";
} else if (val && !/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(val)) {
return "请输入正确身份证号码";
}
};
const rules = [{ validator, message: validatorMessage }];
const onInput = (value) => {};
const onDelete = () => {};
const gender = ref('');
/**
* 按身份证号码获取性别
* @idNumber 身份证号码
* @return 男:male;女:female;异常(身份证号码为空或长度、格式错误):undefined
*/
const getGenderByIdNumber = (idNumber) => {
if (idNumber) {
let genderCode; // 性别代码
if (idNumber.length == 18) { // 二代身份证号码长度为18位(第17位为性别代码)
genderCode = idNumber.charAt(16);
} else if (idNumber.length == 15) { // 一代身份证号码长度为15位(第15位为性别代码)
genderCode = idNumber.charAt(14);
}
if (genderCode && !isNaN(genderCode)) {
// 两代身份证号码的性别代码都为男奇女偶
if (parseInt(genderCode) % 2 == 0) {
return '女';
}
return '男';
}
}
}
</script>
<style lang="less" scoped>
.identity-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
.gender {
padding: 0 1rem 0 1rem;
font-size: 0.9rem;
span {
font-weight: bold;
}
}
}
:deep(.van-field__body) {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
</style>
<!--
* @Date: 2022-08-31 16:16:49
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-01-17 16:44:32
* @FilePath: /data-table/src/components/ImageUploaderField/index.vue
* @Description: 图片上传控件
-->
<template>
<div v-if="HideShow" class="image-uploader-field">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<div
v-if="item.component_props.note"
v-html="item.component_props.note"
style="font-size: 0.9rem; margin-left: 1rem; color: gray; padding-bottom: 0.5rem; padding-top: 0.25rem; white-space: pre;"
/>
<div style="padding: 1rem">
<van-uploader
:name="item.name"
upload-icon="add"
:before-read="beforeRead"
:after-read="afterRead"
:before-delete="beforeDelete"
v-model="fileList"
:multiple="item.component_props.max_size > 1"
/>
</div>
<div class="type-text">上传类型:&nbsp;{{ type_text }}</div>
<div
v-if="show_empty"
class="van-field__error-message"
style="padding: 0 1rem 1rem 1rem"
>
图片上传不能为空
</div>
<van-divider />
</div>
<van-overlay :show="loading">
<div class="wrapper" @click.stop>
<van-loading vertical color="#FFFFFF">上传中...</van-loading>
</div>
</van-overlay>
</template>
<script setup>
/**
* 图片上传
* @param name[String] 组件名称
* @param image_type[Array] 图片上传类型
* @param multiple[Boolean] 图片多选
*/
import { showSuccessToast, showFailToast, showToast } from "vant";
import _ from "lodash";
import { v4 as uuidv4 } from "uuid";
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from "@/api/common";
import BMF from "browser-md5-file";
import { useRoute } from "vue-router";
import axios from "axios";
import { getEtag } from "@/utils/qetag.js"; // 生成hash值
const $route = useRoute();
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const emit = defineEmits(["active"]);
const show_empty = ref(false);
// 固定类型限制
const imageTypes = "jpg/jpeg/png/gif/bmp/psd/tif";
// 文件类型中文页面显示
const type_text = computed(() => {
// return props.item.component_props.image_type;
return imageTypes;
});
// 上传图片集合
const fileList = ref([
// { url: "https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg" },
// Uploader 根据文件后缀来判断是否为图片文件
// 如果图片 URL 中不包含类型信息,可以添加 isImage 标记来声明
// { url: 'https://cloud-image', isImage: true },
]);
// 上传前置处理
const beforeRead = (file) => {
// 类型限制
// const image_types = _.map(
// props.item.component_props.image_type.split("/"),
// (item) => `image/${item}`
// );
const image_types = _.map(imageTypes.split("/"), (item) => `image/${item}`);
let flag = true;
if (_.isArray(file)) {
// 多张图片
const types = _.difference(_.uniq(_.map(file, (item) => item.type)), image_types); // 数组返回不能上传的类型
if (types.length) {
flag = false;
showFailToast("请上传指定格式图片");
}
if (fileList.value.length + file.length > props.item.component_props.max_count) {
// 数量限制
flag = false;
showToast(`最大上传数量为${props.item.component_props.max_count}张`);
}
} else {
if (!_.includes(image_types, file.type)) {
showFailToast("请上传指定格式图片");
flag = false;
}
if (fileList.value.length + 1 > props.item.component_props.max_count) {
// 数量限制
flag = false;
showToast(`最大上传数量为${props.item.component_props.max_count}张`);
}
if ((file.size / 1024 / 1024).toFixed(2) > props.item.component_props.max_size) {
// 体积限制
flag = false;
showToast(
`最大文件体积为${props.item.component_props.max_size}MB`
);
}
}
return flag;
};
// 文件读取完成后的回调函数
const afterRead = async (files) => {
if (Array.isArray(files)) {
// 多张图片上传files是一个数组
muliUpload(files);
} else {
const imgUrl = await handleUpload(files);
// 上传失败提示
if (!imgUrl.src) {
files.status = "failed";
files.message = "上传失败";
loading.value = false;
} else {
files.status = "";
files.message = "";
fileList.value.push({
// meta_id: imgUrl.meta_id,
name: files.file.name,
url: imgUrl.src,
// isImage: true,
});
loading.value = false;
}
}
// 过滤非包含URL的图片
fileList.value = fileList.value.filter((item) => {
if (item.url) return item;
});
props.item.value = {
key: "image_uploader",
filed_name: props.item.key,
// value: fileList.value.map((item) => item.url),
value: fileList.value,
};
show_empty.value = false;
// 完整数据回调到表单上
emit("active", props.item.value);
};
// 文件删除前的回调函数
const beforeDelete = (files) => {
fileList.value = fileList.value.filter((item) => {
if (item.url !== files.url) return item;
});
props.item.value = {
key: "image_uploader",
filed_name: props.item.key,
// value: fileList.value.map((item) => item.url),
value: fileList.value,
};
// 完整数据回调到表单上
emit("active", props.item.value);
};
/********** 上传七牛云获取图片地址 ***********/
const loading = ref(false);
const formCode = $route.query.code; // 表单code
// const uuid = () => {
// let s = [];
// let hexDigits = "0123456789abcdef";
// for (var i = 0; i < 36; i++) {
// s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
// }
// s[14] = "4";
// s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
// s[8] = s[13] = s[18] = s[23] = "-";
// var uuid = s.join("");
// return uuid;
// };
// 上传图片返回图片URL
const handleUpload = async (files) => {
loading.value = true;
// 获取HASH值
// const hash = getEtag(files.content);
return new Promise((resolve, reject) => {
// 获取MD5值
const bmf = new BMF();
bmf.md5(
files.file,
async (err, md5) => {
if (err) {
console.log(err);
reject(err);
}
// 获取七牛token
const filename = files.file.name; // 真实文件名
const getToken = await qiniuTokenAPI({
name: filename,
hash: md5,
});
// 文件上传七牛云
let imgUrl = "";
// 第一次上传
if (getToken.token) {
files.status = "uploading";
files.message = "上传中...";
// 返回数据库真实图片地址
imgUrl = await uploadQiniu(files.file, getToken.token, filename, md5);
}
// 重复上传
if (getToken.data) {
imgUrl = getToken.data;
}
resolve(imgUrl);
},
(process) => {
//计算进度
}
);
});
};
// 多选图片上传遍历
var muliUpload = async (files) => {
for (let item of files) {
const res = await handleUpload(item);
// 上传失败提示
if (!res.src) {
item.status = "failed";
item.message = "上传失败";
loading.value = false;
} else {
item.status = "";
item.message = "";
fileList.value.push({
// meta_id: res.meta_id,
name: item.file.name,
url: res.src,
// isImage: true,
});
loading.value = false;
}
}
};
const getType = (file, name) => {
var index1 = name.lastIndexOf(".");
var index2 = file.length;
var type = file.substring(index1, index2).toUpperCase();
return type;
}
// 生成数据库真实图片地址
const uploadQiniu = async (file, token, name, md5) => {
let suffix = /\.[^\.]+$/.exec(name); // 获取后缀
// let affix = uuidv4();
let fileName = `uploadForm/${formCode}/${md5}${suffix}`;
let formData = new FormData();
formData.append("file", file); // 通过append向form对象添加数据
formData.append("token", token);
formData.append("key", fileName);
let config = {
headers: { "Content-Type": "multipart/form-data" },
};
// 自拍图片上传七牛服务器
let qiniuUploadUrl;
if (window.location.protocol === 'https:') {
qiniuUploadUrl = 'https://up.qbox.me';
} else {
qiniuUploadUrl = 'http://upload.qiniu.com';
}
const { filekey, hash, image_info } = await qiniuUploadAPI(
qiniuUploadUrl,
formData,
config
);
if (filekey) {
// 保存图片
const { data } = await saveFileAPI({
name,
filekey,
hash: md5,
// format: image_info.format,
height: image_info.height,
width: image_info.width,
});
return data;
}
};
/****************** END *******************/
// 校验模块
const validImageUploader = () => {
// 必填项 未上传图片
if (props.item.component_props.required && !fileList.value.length) {
show_empty.value = true;
} else {
show_empty.value = false;
}
return !show_empty.value;
};
defineExpose({ validImageUploader });
</script>
<style lang="less" scoped>
.image-uploader-field {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
.type-text {
font-size: 0.9rem;
margin-left: 1rem;
padding-bottom: 1rem;
color: gray;
}
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.block {
width: 120px;
height: 120px;
background-color: #fff;
}
</style>
<!--
* @Date: 2022-08-29 14:31:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2022-11-23 10:49:41
* @FilePath: /data-table/src/components/MarqueeField/index.vue
* @Description: 跑马灯控件
-->
<template>
<div class="marquee-field-page">
<div class="title">- {{ item.component_props.title }} -</div>
<div class="scroll-wrap">
<div class="scroll-content" :style="{ top }">
<p v-for="x in marqueeList">
<van-row>
<van-col span="8">{{ x.name }}</van-col>
<van-col span="8">{{ x.tel }}</van-col>
<van-col span="8">{{ x.time }}</van-col>
</van-row>
</p>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
item: Object,
});
onMounted(() => {
getList();
ScrollUp();
});
const top = computed(() => {
return -activeIndex.value * 30 + "px";
});
const activeIndex = ref(0);
const intNum = ref(0);
const marqueeList = ref([]);
// 查询名单
// TODO: 数据从哪里来?
const getList = () => {
const arr = [];
for (let index = 0; index < 100; index++) {
arr.push({
name: `abc${index}`,
tel: "137***3456",
time: `${index}分钟前`,
});
}
console.warn(arr);
marqueeList.value = arr;
};
//滚动播报方法
const ScrollUp = () => {
intNum.value = setInterval(() => {
if (activeIndex.value < marqueeList.value.length) {
activeIndex.value += 1;
} else {
activeIndex.value = 0;
}
}, 1000);
};
</script>
<style lang="less" scoped>
.marquee-field-page {
padding-bottom: 1rem;
.title {
font-weight: bold;
text-align: center;
width: 100%;
font-size: 1rem;
padding: 1rem 0;
}
.scroll-wrap {
position: relative;
z-index: 2;
overflow: hidden;
.scroll-content {
position: relative;
transition: top 0.5s;
height: 10rem;
}
.scroll-content p {
/* 每行信息间隔高度 */
line-height: 2;
font-size: 1rem;
text-align: center;
}
}
}
</style>
<!--
* @Date: 2022-08-30 11:34:19
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-10 14:41:52
* @FilePath: /data-table/src/components/MultiRuleField/index.vue
* @Description: 多选规则确认控件
-->
<template>
<div v-if="HideShow" class="multi-rule-field-page">
<div class="label">
<span v-if="item.component_props.required" class="required">&nbsp;*</span>
{{ item.component_props.label }}
<span v-if="item.component_props.min_count" style="color: #999; font-size: 0.85rem;">(最少选{{ item.component_props.min_count }}项)</span>
</div>
<van-field :name="item.key" :rules="rules" :border="false" style="padding-bottom: 0">
<template #input>
<van-checkbox-group v-model="item.value" style="width: 100%">
<template v-for="(rule, idx) in item.component_props.options" :key="idx">
<div class="multi-rule-field-box">
<van-checkbox
:name="rule.value"
shape="square"
:checked-color="themeVars.radioColor"
>{{ rule.title }}</van-checkbox
>
<div class="van-multi-ellipsis--l3 rule-desc-text">{{ rule.desc_text }}</div>
<div v-if="rule.desc_type === 'text'" class="rule-box" @click="showRule(rule)">
{{ rule.desc_btn_name }}&nbsp;>>
</div>
<div v-if="rule.desc_type === 'url'" class="rule-box" @click="showUrl(rule)">
{{ rule.desc_btn_name }}&nbsp;<van-icon name="link-o" />
</div>
</div>
</template>
</van-checkbox-group>
</template>
</van-field>
</div>
<van-overlay :show="show" :lock-scroll="false">
<div class="rule-wrapper" @click.stop>
<div class="rule-block">
<div style="height: 70vh; min-height: 70vh; overflow: scroll; white-space: pre-wrap; line-height: 1.5;" v-html="rule_content"></div>
<div class="close-btn">
<van-button type="primary" block @click="closeRule"
>关&nbsp;&nbsp;闭</van-button
>
</div>
<div></div>
</div>
</div>
</van-overlay>
</template>
<script setup>
import { styleColor } from "@/constant.js";
import $ from "jquery";
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
// TAG: 自定义主题颜色
const themeVars = {
radioColor: styleColor.baseColor,
};
onMounted(() => {
$(".rule-box").css("color", themeVars.radioColor);
});
const show = ref(false);
const checked = ref([]);
const showRule = (rule) => {
show.value = true;
rule.desc_text = rule.desc_text.replace(/\\n/g, "<br>")
rule_content.value = rule.desc_text;
};
const closeRule = () => {
show.value = false;
rule_content.value = "";
};
const showUrl = (rule) => {
location.href = rule.desc_url
}
const rule_content = ref("");
const required = props.item.component_props.required;
const min_count = props.item.component_props.min_count;
const max_count = props.item.component_props.max_count;
const validator = (val) => {
if (required && !val.length) {
return false;
} else if (min_count && props.item.value.length < min_count) {
return false;
} else if (max_count && props.item.value.length > max_count) {
return false;
} else {
return true;
}
};
// 错误提示文案
const validatorMessage = (val, rule) => {
if (required && !val.length) {
return "选择项不能为空";
} else if (min_count && props.item.value.length < min_count) {
return `最少选择${min_count}项`;
} else if (max_count && props.item.value.length > max_count) {
return `最多选择${max_count}项`;
}
};
const rules = [{ validator, message: validatorMessage }];
</script>
<style lang="less" scoped>
.multi-rule-field-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span.required {
color: red;
}
}
.rule-desc-text {
margin: 0.35rem 0.5rem 0.5rem 1.75rem;
font-size: 0.8rem;
color: #808080;
}
.rule-box {
font-size: 0.85rem;
margin-left: 1.8rem;
padding-bottom: 0.5rem;
width: fit-content;
}
}
.rule-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.rule-block {
position: relative;
width: 80vw;
height: 80vh;
background-color: #fff;
overflow: scroll;
padding: 1rem;
.close-btn {
position: absolute;
bottom: 1rem;
// transform: translateX(100%);
width: calc(100% - 2rem);
}
}
.multi-rule-field-box {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 1rem 0.5rem 0 0.5rem;
// width: 100vw;
margin-bottom: 0.5rem;
}
</style>
<!--
* @Date: 2022-08-29 14:31:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-01 11:21:00
* @FilePath: /data-table/src/components/NameField/index.vue
* @Description: 姓名输入框
-->
<template>
<div v-if="HideShow" class="name-field-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<van-field
v-model="item.value"
:name="item.name"
:type="item.type"
:placeholder="item.component_props.placeholder ? item.component_props.placeholder : '请输入'"
:rules="item.rules"
:required="item.required"
:readonly="item.component_props.readonly"
:disabled="item.component_props.disabled"
:input-align="item.component_props.align"
/>
</div>
</template>
<script setup>
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
onMounted(() => {
props.item.value = props.item.component_props.default;
})
</script>
<style lang="less" scoped>
.name-field-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
}
:deep(.van-field__body) {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
</style>
<!--
* @Date: 2022-08-29 14:31:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-03-08 15:28:33
* @FilePath: /data-table/src/components/NoteField/index.vue
* @Description: 富文本组件
-->
<template>
<div v-if="HideShow" class="note-field-page">
<div class="label">
<span v-if="item.component_props.required" class="required">&nbsp;*</span>
<span v-if="!item.component_props.hide_label">{{ item.component_props.label }}</span>
</div>
<div class="html" style="padding: 0.5rem 1rem 0 1rem;" v-html="item.component_props.note"></div>
</div>
</template>
<script setup>
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
</script>
<style lang="less">
.note-field-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span.required {
color: red;
}
}
.html {
img {
width: 100%;
}
}
}
</style>
<!--
* @Date: 2022-09-14 14:44:30
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-09 15:54:02
* @FilePath: /data-table/src/components/NumberField/index.vue
* @Description: 数字输入框
-->
<template>
<div v-if="HideShow" class="number-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<div
v-if="item.component_props.note"
v-html="item.component_props.note"
style="font-size: 0.9rem; margin-left: 1rem; color: gray; padding-bottom: 0.5rem; padding-top: 0.25rem; white-space: pre-wrap;"
/>
<van-field
v-model="item.value"
:id="item.name"
:name="item.name"
:placeholder="item.component_props.placeholder"
:rules="rules"
:required="item.component_props.required"
:disabled="item.component_props.readonly"
readonly
@touchstart.stop="showKeyboard($event)"
:border="false"
>
</van-field>
<van-number-keyboard
v-model="item.value"
:show="showInteger"
@blur="blurKeyboard()"
@input="onInput"
@delete="onDelete"
safe-area-inset-bottom
/>
<van-number-keyboard
v-model="item.value"
:show="showDecimal"
theme="custom"
extra-key="."
close-button-text="完成"
@blur="blurKeyboard()"
@input="onInput"
@delete="onDelete"
safe-area-inset-bottom
/>
</div>
</template>
<script setup>
import $ from "jquery";
import { storeToRefs, mainStore } from "@/utils/generatePackage";
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
let content = "";
const store = mainStore();
const { fieldName } = storeToRefs(store);
// 监听字段变化
watch(
() => fieldName.value,
(v) => {
// 如果不是点击本输入框
if (v !== props.item.name) {
// 还原border颜色
$(`#${props.item.name}`).parent().css("border-color", "#eaeaea");
if (props.item.component_props.max_fraction_count === 0) {
// 显示整数键盘
showInteger.value = false;
} else {
// 显示小数键盘
showDecimal.value = false;
}
document.getElementById("app").style.paddingBottom = "0";
}
}
);
const readonly = props.item.component_props.readonly;
const showKeyboard = (e) => {
if (readonly) return false; // 如果为只读,不能设置
// 键盘上移动
const target_to_view_height =
window.innerHeight - e.target.getBoundingClientRect().bottom; // 元素到适口高度
const target_top = document.body.scrollHeight - $(e.target).offset().top; // 元素到正文高度
let scroll_height = "";
if (target_to_view_height <= 250) {
document.getElementById("app").style.paddingBottom = "250px";
// 向上滚动位置
document.documentElement.scrollTop = $(e.target).offset().top - 244;
}
// if (target_top < 250) {
// window.scrollTo(0, $("#app").height());
// } else {
// // 向上滚动位置
// document.documentElement.scrollTop = (target_top > 500 ? 0 : target_top) + 250;
// }
// 选中添加border颜色
content = $(e.target).parent();
// TAG: 自定义主题颜色
content.css("border-color", "#c2915f");
setTimeout(() => {
if (props.item.component_props.max_fraction_count === 0) {
// 显示整数键盘
showInteger.value = true;
} else {
// 显示小数键盘
showDecimal.value = true;
}
}, 300);
// 记录点击field名
store.changeFieldName(props.item.name);
};
const blurKeyboard = () => {
if (props.item.component_props.max_fraction_count === 0) {
// 显示整数键盘
showInteger.value = false;
} else {
// 显示小数键盘
showDecimal.value = false;
}
document.getElementById("app").style.paddingBottom = "0";
// 还原border颜色
content.css("border-color", "#eaeaea");
};
const showDecimal = ref(false);
const showInteger = ref(false);
// 校验函数返回 true 表示校验通过,false 表示不通过
const required = props.item.component_props.required;
const min = props.item.component_props.min;
const max = props.item.component_props.max;
const max_count = props.item.component_props.max_fraction_count; // 保留小数个数 null=不限,0=没有小数,大于0=最多只能输入的小数个数
const reg = new RegExp("^([0-9]+)(\\.(\\d){" + max_count +"," + max_count +"})$", "g");
const validator = (val) => {
if (required && !val) { // 必填
return false;
} else if (val && min && (val < min)) { // 小于最小值
return false;
} else if (val && max && (val > max)) { // 大于最大值
return false;
} else if (val && max_count && !reg.test(val)) { // 不符合保留小数个数
return false;
} else {
return true;
}
};
// 错误提示文案
const validatorMessage = (val, rule) => {
if (required && !val) {
return "必填项不能为空";
} else if (val && min && (val < min)) { // 小于最小值
return "最小值为" + min;
} else if (val && max && (val > max)) { // 大于最大值
return "最大值为" + max;
} else if (val && max_count && !reg.test(val)) { // 不符合保留小数个数
return "保留小数点后" + max_count + "位";
}
};
const rules = [{ validator, message: validatorMessage }];
const onInput = (value) => {};
const onDelete = () => {};
</script>
<style lang="less" scoped>
.number-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
}
:deep(.van-field__body) {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
input {
color: #323233;
}
}
:deep(.van-number-keyboard__title) {
font-size: 1.05rem;
}
</style>
<!--
* @Date: 2022-09-02 10:46:03
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-03-20 07:45:28
* @FilePath: /data-table/src/components/PhoneField/index.vue
* @Description: 手机输入框
-->
<template>
<div v-if="HideShow" class="phone-field-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<!-- <van-field
:id="item.name"
v-model="item.value"
:name="item.name"
type="digit"
maxlength="11"
:placeholder="item.component_props.placeholder ? item.component_props.placeholder : '请输入手机号码'"
:rules="rules"
:required="item.component_props.required"
:disabled="item.component_props.readonly"
:readonly="readonly"
@touchstart.stop="openKeyboard($event)"
:border="false"
> -->
<van-field
:id="item.name"
v-model="item.value"
:name="item.name"
type="digit"
maxlength="11"
:placeholder="item.component_props.placeholder ? item.component_props.placeholder : '请输入手机号码'"
:rules="rules"
:required="item.component_props.required"
:disabled="item.component_props.readonly"
:border="false"
>
</van-field>
<van-field
v-if="is_sms"
name="ignore"
v-model="sms"
center
clearable
label=""
placeholder="请输入短信验证码"
:border="false"
>
<template #button>
<van-button size="small" type="primary">发送验证码</van-button>
</template>
</van-field>
<van-number-keyboard
v-model="item.value"
:show="show"
:maxlength="11"
@blur="blurKeyboard()"
safe-area-inset-bottom
/>
</div>
</template>
<script setup>
import { wxInfo } from "@/utils/tools";
import $ from "jquery";
import { storeToRefs, mainStore } from "@/utils/generatePackage";
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
// web端判断
const readonly = computed(() => wxInfo().isMobile);
// 打开短信验证模式
const is_sms = ref(false);
// 校验函数返回 true 表示校验通过,false 表示不通过
const validator = (val) => {
if (props.item.component_props.required && !val) {
return false;
} else if (val && !/1\d{10}/.test(val)) {
return false;
} else {
return true;
}
};
// 错误提示文案
const validatorMessage = (val, rule) => {
if (props.item.component_props.required && !val) {
return "手机号码不能为空";
} else if (val && !/1\d{10}/.test(val)) {
return "请输入正确手机号码";
}
};
const rules = [{ validator, message: validatorMessage }];
const show = ref(false);
let content = "";
const store = mainStore();
const { fieldName } = storeToRefs(store);
// 监听字段变化
watch(
() => fieldName.value,
(v) => {
// 如果不是点击本输入框
if (v !== props.item.name) {
// 还原border颜色
$(`#${props.item.name}`).parent().css("border-color", "#eaeaea");
show.value = false;
document.getElementById("app").style.paddingBottom = "0";
}
}
);
const openKeyboard = (e) => {
if (props.item.component_props.readonly) return false; // 如果为只读,不能设置
// 键盘上移动
const target_to_view_height =
window.innerHeight - e.target.getBoundingClientRect().bottom; // 元素到适口高度
const target_top = document.body.scrollHeight - $(e.target).offset().top; // 元素到正文高度
let scroll_height = "";
if (target_to_view_height <= 250) {
document.getElementById("app").style.paddingBottom = "250px";
// 向上滚动位置
document.documentElement.scrollTop = $(e.target).offset().top - 244;
}
// 选中添加border颜色
content = $(e.target).parent();
// TAG: 自定义主题颜色
content.css("border-color", "#c2915f");
setTimeout(() => {
show.value = true;
}, 300);
// 记录点击field名
store.changeFieldName(props.item.name);
};
const blurKeyboard = () => {
show.value = false;
document.getElementById("app").style.paddingBottom = "0";
// 还原border颜色
content.css("border-color", "#eaeaea");
};
</script>
<style lang="less" scoped>
.phone-field-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
:deep(.van-field__body) {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
input {
color: #323233;
}
}
}
</style>
<!--
* @Date: 2022-08-30 13:46:51
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-03-03 21:11:24
* @FilePath: /data-table/src/components/PickerField/index.vue
* @Description: 单列选择器组件
-->
<template>
<div v-if="HideShow" class="picker-field-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<van-field
v-model="picker_value"
is-link
readonly
:name="item.key"
:required="item.component_props.required"
:placeholder="item.component_props.placeholder"
:rules="item.rules"
@click="showPicker = true"
:border="false"
/>
<!-- <van-field v-if="has_add_info" :name="add_info_name" v-model="add_info" label="" placeholder="请输入补充信息" :border="false" style="border: 1px solid #eaeaea;border-radius: 0.25rem; padding: 0.25rem 0.5rem; margin-top: 0.25rem;" /> -->
<van-popup v-model:show="showPicker" position="bottom">
<van-picker
:columns="item.component_props.options"
@confirm="onConfirm"
@cancel="showPicker = false"
/>
</van-popup>
</div>
</template>
<script setup>
const props = defineProps({
item: Object,
});
const emit = defineEmits(["active"]);
// 绑定值发生变化时回调,处理选项为其他时的输入项录入
// const has_add_info = ref(false); // TODO: 文字不一定是其他,后续可能需要字段绑定一个值,标识是否有其他输入框进行判断
// const add_info = ref('');
// const add_info_name = ref(props.item.key + '#');
// const add_info_key = ref('其他'); // TODO: 以后动态获取
onMounted(() => {
// add_info_name.value = `${props.item.key}#${add_info_key.value}`
// props.item.component_props.options = props.item.component_props.options.map((opt) => {
// return {
// text: opt,
// value: opt,
// };
// });
});
const selectedValues = ref("");
const showPicker = ref(false);
const picker_value = ref(props.item.component_props.default);
const onConfirm = ({ selectedOptions }) => {
picker_value.value = selectedOptions[0]?.value;
showPicker.value = false;
// 触发点自定义监听事件,配合规则显示隐藏其他字段
props.item.value = { key: props.item.key, value: picker_value.value, type: "picker" };
emit("active", props.item.value);
// if (add_info_key.value === props.item.value) {
// has_add_info.value = true;
// }
};
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
</script>
<style lang="less" scoped>
.picker-field-page {
margin: 1rem;
.label {
// padding: 1rem 1rem 0 0;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
}
:deep(.van-cell--clickable) {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
margin-top: 0.5rem;
}
</style>
<!--
* @Date: 2022-08-30 11:34:19
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-22 11:02:05
* @FilePath: /data-table/src/components/RadioField/index.vue
* @Description: 单项选择控件
-->
<template>
<div v-if="HideShow" class="radio-field-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<div v-if="item.component_props.note" class="note" v-html="item.component_props.note" />
<van-field :rules="item.rules">
<template #input>
<van-radio-group @change="onChange(item)" v-model="radio_value" :direction="item.component_props.direction"
style="width: 100%">
<div v-for="x in item.component_props.options" :key="x.value" class="radio-wrapper">
<van-radio :name="x.value" icon-size="1rem" :checked-color="themeVars.radioColor"
style="margin-bottom: 0.25rem">{{ x.title }}</van-radio>
<div v-if="x.desc_text" class="van-multi-ellipsis--l3 rule-desc-text">{{ x.desc_text }}</div>
<div v-if="x.desc_type === 'text'" class="rule-box" @click="showRule(x)">
{{ x.desc_btn_name }}&nbsp;>>
</div>
<div v-if="x.desc_type === 'url'" class="rule-box" @click="showUrl(x)">
{{ x.desc_btn_name }}&nbsp;<van-icon name="link-o" />
</div>
<van-field v-if="radio_value === x.value && x.is_input" @blur="onBlur(x)" v-model="x.affix" label=" " label-width="5px"
:placeholder="x.input_placeholder" :rules="x.input_required ? rules : ''" :required="x.input_required"
class="affix-input" />
</div>
</van-radio-group>
</template>
</van-field>
</div>
<van-overlay :show="show" :lock-scroll="false">
<div class="rule-wrapper" @click.stop>
<div class="rule-block">
<div style="height: 70vh; min-height: 70vh; overflow: scroll; white-space: pre-wrap; line-height: 1.5;" v-html="rule_content"></div>
<div class="close-btn">
<van-button type="primary" block @click="closeRule"
>关&nbsp;&nbsp;闭</van-button
>
</div>
<div></div>
</div>
</div>
</van-overlay>
</template>
<script setup>
import { styleColor } from "@/constant.js";
import $ from "jquery";
const props = defineProps({
item: Object,
});
// TAG: 自定义主题颜色
const themeVars = {
radioColor: styleColor.baseColor,
};
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
// TODO: 等待数据结构更新,看看怎么判断必填
// 校验函数返回 true 表示校验通过,false 表示不通过
const validator = (val) => {
if (!val) {
return false;
} else {
return true;
}
};
// 错误提示文案
const validatorMessage = (val, rule) => {
if (!val) {
return "补充信息不能为空";
}
};
const rules = [{ validator, message: validatorMessage }];
const emit = defineEmits(["active"]);
const radio_value = ref(props.item.component_props.default);
const affix_value = ref('');
const onChange = (item) => {
clearAffix()
// 发送自定义数据结构
props.item.value = { key: props.item.key, value: radio_value.value, affix: affix_value.value, type: "radio" };
emit("active", props.item.value);
}
const onBlur = (item) => {
clearAffix()
affix_value.value = item.affix ? `${item.title}: ${item.affix}` : '';
// 发送自定义数据结构
props.item.value = { key: props.item.key, value: radio_value.value, affix: affix_value.value, type: "radio" };
emit("active", props.item.value);
}
const clearAffix = () => {
const options = props.item.component_props.options;
// 为选中项目的补充清空
options.forEach(element => {
if (element.value !== radio_value.value) {
element.affix = ''
}
});
}
onMounted(() => {
radio_value.value = props.item.component_props.default ? props.item.component_props.default : '';
// 发送自定义数据结构
props.item.value = { key: props.item.key, value: radio_value.value, affix: affix_value.value, type: "radio" };
emit("active", props.item.value);
//
$(".rule-box").css("color", themeVars.radioColor);
})
// 补充信息弹框
const show = ref(false);
const showRule = (rule) => {
show.value = true;
rule.desc_text = rule.desc_text.replace(/\\n/g, "<br>")
rule_content.value = rule.desc_text;
};
const closeRule = () => {
show.value = false;
rule_content.value = "";
};
const showUrl = (rule) => {
location.href = rule.desc_url
}
const rule_content = ref("");
</script>
<style lang="less" scoped>
.radio-field-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
.note {
font-size: 0.9rem;
margin-left: 1rem;
color: gray;
padding-bottom: 0.5rem;
white-space: pre-wrap;
}
.radio-wrapper {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
margin-bottom: 0.25rem;
}
.affix-input {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
margin-top: 0.5rem;
margin-bottom: 0.25rem;
}
.rule-desc-text {
margin: 0.35rem 0.5rem 0.5rem 1.75rem;
font-size: 0.8rem;
color: #808080;
}
.rule-box {
font-size: 0.85rem;
margin-left: 1.8rem;
padding-bottom: 0.5rem;
width: fit-content;
}
}
.rule-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.rule-block {
position: relative;
width: 80vw;
height: 80vh;
background-color: #fff;
overflow: scroll;
padding: 1rem;
.close-btn {
position: absolute;
bottom: 1rem;
// transform: translateX(100%);
width: calc(100% - 2rem);
}
}
.multi-rule-field-box {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 1rem 0.5rem 0 0.5rem;
// width: 100vw;
margin-bottom: 0.5rem;
}
:deep(.van-radio) {
// border: 1px solid #eaeaea;
// border-radius: 0.25rem;
// padding: 0.25rem 0.5rem;
}
</style>
<!--
* @Date: 2022-09-08 15:47:54
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-01-31 22:57:40
* @FilePath: /data-table/src/components/RatePickerField/index.vue
* @Description: 评分选择控件
-->
<template>
<div v-if="HideShow" class="rate-field">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<van-rate
v-model="rate_value"
:count="item.component_props.data_length"
:readonly="item.component_props.readonly"
:color="styleColor.baseColor"
@change="onChange"
style="padding: 1rem"
/>
<div
v-if="show_empty"
class="van-field__error-message"
style="padding: 0 1rem 1rem 1rem"
>
评分不能为空
</div>
<van-divider />
</div>
</template>
<script setup>
import { styleColor } from "@/constant.js";
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const emit = defineEmits(["active"]);
const show_empty = ref(false);
const rate_value = ref(props.item.component_props.default);
const onChange = (value) => {
props.item.value = { key: props.item.key, value, type: "rate" };
emit("active", props.item.value);
};
onMounted(() => {
props.item.value = { key: props.item.key, value: rate_value.value, type: "rate" };
emit("active", props.item.value);
});
const validRate = () => {
// 必填项
if (props.item.component_props.required && !rate_value.value) {
show_empty.value = true;
} else {
show_empty.value = false;
}
return !show_empty.value;
};
defineExpose({ validRate });
</script>
<style lang="less" scoped>
.rate-field {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
}
</style>
<!--
* @Date: 2022-08-30 11:34:19
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-09 16:07:40
* @FilePath: /data-table/src/components/RuleField/index.vue
* @Description: 规则确认控件
-->
<template>
<div class="rule-field-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<van-field
:name="item.key"
:rules="item.rules"
:border="false"
style="padding-bottom: 0"
>
<template #input>
<van-radio-group v-model="item.value">
<van-radio name="1">{{ item.component_props.rule_desc }}</van-radio>
</van-radio-group>
</template>
</van-field>
<div class="rule-box" @click="show = true">
{{ item.component_props.rule_link }}&nbsp;>>
</div>
</div>
<van-overlay :show="show" @click="show = false" :lock-scroll="false">
<div class="rule-wrapper" @click.stop>
<div class="rule-block">
<div
style="height: 70vh; overflow: scroll"
v-html="item.component_props.rule_content"
></div>
<div class="close-btn">
<van-button type="primary" block @click="show = false"
>关&nbsp;&nbsp;闭</van-button
>
</div>
<div></div>
</div>
</div>
</van-overlay>
</template>
<script setup>
const props = defineProps({
item: Object,
});
const show = ref(false);
</script>
<style lang="less" scoped>
.rule-field-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
.rule-box {
font-size: 0.85rem;
margin-left: 2.7rem;
padding-bottom: 1rem;
color: #1989fa;
}
}
.rule-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.rule-block {
position: relative;
width: 80vw;
height: 80vh;
background-color: #fff;
overflow: scroll;
padding: 1rem;
.close-btn {
position: absolute;
bottom: 1rem;
// transform: translateX(100%);
width: calc(100% - 2rem);
}
}
</style>
<!--
* @Date: 2022-09-06 16:29:31
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-03-23 15:50:45
* @FilePath: /data-table/src/components/SignField/index.vue
* @Description: 电子签名控件
-->
<template>
<div v-if="HideShow" class="sign-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
{{ valid }}
</div>
<div ref="wrapperRef" class="esign-wrapper">
<!-- <div style="padding: 1rem; position: relative; height: 150px; background-color: #FCFCFC;border: 1px solid #EAEAEA; border-radius: 5px;"> -->
<vue-esign v-if="esignWidth" ref="esign" class="sign-wrapper" :width="esignWidth" :height="esignHeight" :isCrop="isCrop"
:lineWidth="lineWidth" :lineColor="lineColor" :bgColor.sync="bgColor" />
<div v-if="show_sign" class="whiteboard">
<div class="text" @click="startSign">
<van-icon name="edit" />&nbsp;点击开始签署电子签名
</div>
</div>
</div>
<div v-if="!show_sign">
<div v-if="show_control" class="control-sign">
<van-row gutter="20" style="padding: 0 1rem">
<van-col :span="12">
<van-button type="default" block @click="handleGenerate">确认签名</van-button>
</van-col>
<van-col :span="12">
<van-button type="default" block @click="cancelSign">取消签名</van-button>
</van-col>
</van-row>
</div>
<div v-else style="padding: 0 1rem">
<van-button type="danger" block @click="handleReset">删除签名</van-button>
</div>
</div>
<div v-if="show_empty" class="van-field__error-message" style="padding: 0 1rem 1rem 1rem">
电子签名不能为空
</div>
<van-divider />
</div>
<van-overlay :show="loading">
<div class="wrapper" @click.stop>
<van-loading vertical color="#FFFFFF">生成中...</van-loading>
</div>
</van-overlay>
</template>
<script setup>
import { v4 as uuidv4 } from "uuid";
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from "@/api/common";
import { showSuccessToast, showFailToast } from "vant";
import { useRoute } from "vue-router";
import BMF from "browser-md5-file";
import { getEtag } from "@/utils/qetag.js"; // 生成hash值
import dayjs from "dayjs";
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const $route = useRoute();
const emit = defineEmits(["active"]);
const esign = ref(null);
let esignWidth = ref();
let esignHeight = ref();
const wrapperRef = ref(null)
onMounted(() => {
// 动态计算画板canvas宽度/高度
setTimeout(() => {
esignWidth.value = wrapperRef.value.offsetWidth - 32;
esignHeight.value = (window.innerHeight) / 5;
}, 100);
})
const lineWidth = ref(6);
const lineColor = ref("#000000");
const bgColor = ref("#FCFCFC");
const isCrop = ref(false);
const show_control = ref(true);
const image_url = ref("");
const show_empty = ref(false);
const handleReset = () => {
// 清空画板
esign.value.reset();
show_control.value = true;
// 删除可能存在的签名
image_url.value = "";
props.item.value = {
key: "sign",
filed_name: props.item.key,
value: "",
};
emit("active", props.item.value);
};
/********** 上传七牛云获取图片地址 ***********/
const loading = ref(false);
const formCode = $route.query.code; // 表单code
const uuid = () => {
let s = [];
let hexDigits = "0123456789abcdef";
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4";
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = "-";
var uuid = s.join("");
return uuid;
};
const uploadQiniu = async (file, token, filename) => {
let formData = new FormData();
formData.append("file", file); // 通过append向form对象添加数据
formData.append("token", token);
formData.append("key", filename);
let config = {
headers: { "Content-Type": "multipart/form-data" },
};
// 自拍图片上传七牛服务器
let qiniuUploadUrl;
if (window.location.protocol === 'https:') {
qiniuUploadUrl = 'https://up.qbox.me';
} else {
qiniuUploadUrl = 'http://upload.qiniu.com';
}
const { filekey, hash, image_info } = await qiniuUploadAPI(
qiniuUploadUrl,
formData,
config
);
if (filekey) {
// 保存图片
const { data } = await saveFileAPI({
filekey,
hash,
format: image_info.format,
height: image_info.height,
width: image_info.width,
});
return data;
}
};
/****************** END *******************/
const handleUpload = async (files, filename) => {
// 上传图片流程
loading.value = true;
// 获取HASH值
const hash = getEtag(files);
// 获取七牛token
const { token, key, code } = await qiniuTokenAPI({
name: filename,
hash,
});
// 文件上传七牛云
const imgUrl = await uploadQiniu(files, token, filename);
return imgUrl;
};
const handleGenerate = () => {
esign.value
.generate()
.then(async (res) => {
let affix = uuidv4();
let fileName = `uploadForm/${formCode}/${affix}_sign.png`;
let file = dataURLtoFile(res, fileName); // 生成文件
const imgUrl = await handleUpload(file, fileName);
loading.value = false;
props.item.value = {
key: "sign",
filed_name: props.item.key,
value: { name: `电子签名${dayjs().format('YYYYMMDDHHmmss')}.png`, url: imgUrl.src},
};
image_url.value = imgUrl.src;
show_control.value = false;
show_empty.value = false;
emit("active", props.item.value);
})
.catch((err) => {
loading.value = false;
// 签名生成失败
console.warn(err);
if (err) {
showFailToast("签名生成失败");
}
});
};
//将图片base64转换为文件
const dataURLtoFile = (dataurl, filename) => {
var arr = dataurl.split(","),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
};
const show_sign = ref(true);
const startSign = () => {
show_sign.value = false;
show_empty.value = false;
};
const cancelSign = () => {
show_sign.value = true;
show_empty.value = false;
handleReset();
};
const validSign = () => {
// 必填项 未生成签名
if (props.item.component_props.required && !image_url.value) {
show_empty.value = true;
} else {
show_empty.value = false;
}
return !show_empty.value;
};
defineExpose({ validSign });
</script>
<!-- <script>
export default {
methods: {
validSign () {
console.warn(0);
}
}
}
</script> -->
<style lang="less" scoped>
.sign-page {
// padding-bottom: 1rem;
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
.esign-wrapper {
padding: 1rem;
position: relative;
box-sizing: border-box;
.sign-wrapper {
border: 1px solid #eaeaea;
border-radius: 5px;
}
.whiteboard {
position: absolute;
height: 100%;
width: 100%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
text-align: center;
.text {
position: absolute;
width: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
.control-sign {
padding-bottom: 1rem;
}
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.block {
width: 120px;
height: 120px;
background-color: #fff;
}
</style>
<!--
* @Date: 2022-08-29 14:31:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2022-08-29 16:44:53
* @FilePath: /data-table/src/components/TableField/index.vue
* @Description: 文件描述
-->
<template>
<van-field v-if="item.component_type === 'radio'" :name="item.key" :label="item.label">
<template #input>
<van-radio-group v-model="item.value" direction="horizontal">
<van-radio v-for="x in item.sub" :key="index" :name="x.key">{{ x.value }}</van-radio>
</van-radio-group>
</template>
</van-field>
<van-field v-else v-model="item.value" :name="item.name" :label="item.label" :type="item.type"
:placeholder="item.placeholder" :rules="item.rules" :required="item.required" :autosize="item.autosize"
:row="item.row" />
</template>
<script setup>
const props = defineProps({
item: Object
})
</script>
<style lang="less" scoped>
</style>
<!--
* @Date: 2022-08-29 14:31:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-03-24 15:04:18
* @FilePath: /custom_form/src/components/TextField/index.vue
* @Description: 单行文本输入框(微信扫描功能)
-->
<template>
<div v-if="HideShow" class="text-field-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<div class="note-wrapper" v-if="item.component_props.note" v-html="item.component_props.note" />
<nut-input v-model="item.value" :name="item.name" :type="item.type"
:placeholder="item.component_props.placeholder ? item.component_props.placeholder : '请输入'" :rules="item.rules"
:required="item.required"
:readonly="item.component_props.readonly || (item.component_props.is_camera_scan && !item.component_props.is_edit_camera_scan_result)"
:disabled="item.component_props.disabled" :input-align="item.component_props.align"
:right-icon="item.component_props.is_camera_scan ? 'scan' : ''" @click-right-icon="clickRightIcon" />
</div>
</template>
<script setup>
import { getUrlParams } from "@/utils/tools";
import Taro from '@tarojs/taro'
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
// 默认识别类型
const scan_type_code = ref('ALL_CODE');
// 微信二维码扫描功能判断
const scanType = (scan_type_code) => {
switch (scan_type_code) {
case 'ALL_CODE':
return ["qrCode", "barCode"]
case 'QR_CODE':
return ["qrCode"]
case 'BAR_CODE':
return ["barCode"]
}
}
// 预览模式
const model = getUrlParams(location.href) ? getUrlParams(location.href).model : '';
const clickRightIcon = async () => {
// 预览模式屏蔽微信功能
if (model === 'preview') return false;
Taro.ready(() => {
Taro.scanQRCode({
needResult: 1, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
scanType: scanType(props.item.camera_scan_type), // 可以指定扫二维码还是一维码,默认二者都有
success: function (res) {
if (res.errMsg === 'scanQRCode:ok') {
let code = res.resultStr;
let code_arr = code.split(",");
props.item.value = code_arr[1];
Taro.showToast({
title: '扫描成功',
icon: 'success',
duration: 2000
})
} else {
console.warn(res);
Taro.showToast({
title: '扫描失败',
icon: 'error',
duration: 2000
})
}
},
error: function (res) {
if (res.errMsg.indexOf('function_not_exist') > 0) {
alert('版本过低请升级')
}
alert(res.errMsg);
},
});
});
}
</script>
<style lang="less" scoped>
.text-field-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
.note-wrapper {
font-size: 0.9rem;
margin-left: 1rem;
color: gray;
padding-bottom: 0.5rem;
padding-top: 0.25rem;
white-space: pre-wrap;
}
}
}
:deep(.van-field__body) {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
</style>
<!--
* @Date: 2022-08-29 14:31:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-03-22 09:13:58
* @FilePath: /data-table/src/components/TextareaField/index.vue
* @Description: 多行文本输入框
-->
<template>
<div v-if="HideShow" class="textarea-field-page">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<div
v-if="item.component_props.note"
v-html="item.component_props.note"
style="font-size: 0.9rem; margin-left: 1rem; color: gray; padding-bottom: 0.5rem; padding-top: 0.25rem; white-space: pre-wrap;"
/>
<van-field
v-model="item.value"
:name="item.name"
:type="item.type"
:placeholder="item.component_props.placeholder ? item.component_props.placeholder : '请输入'"
:rules="item.rules"
:required="item.component_props.required"
:readonly="item.component_props.readonly"
:rows="item.component_props.rows"
autosize
:maxlength="item.component_props.maxlength ? item.component_props.maxlength : null"
:show-word-limit="item.component_props.maxlength"
/>
</div>
</template>
<script setup>
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
</script>
<style lang="less" scoped>
.textarea-field-page {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
}
// :deep(.van-field__body) {
// border: 1px solid #eaeaea;
// border-radius: 0.25rem;
// padding: 0.25rem 0.5rem;
// }
:deep(.van-cell__value) {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
</style>
<!--
* @Date: 2022-08-31 11:45:30
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-02-10 10:11:05
* @FilePath: /data-table/src/components/TimePickerField/index.vue
* @Description: 时间选择组件
-->
<template>
<div v-if="HideShow" class="time-picker-field">
<div class="label">
<span v-if="item.component_props.required">&nbsp;*</span>
{{ item.component_props.label }}
</div>
<van-field
v-model="item.value"
is-link
readonly
:name="item.key"
:required="item.component_props.required"
:disabled="item.component_props.readonly"
:placeholder="item.component_props.placeholder ? item.component_props.placeholder : '请选择时间'"
:rules="rules"
@click="onTap"
:border="false"
/>
<van-popup v-model:show="showPicker" position="bottom">
<van-time-picker
v-model="currentTime"
title="请选择时间"
:columns-type="columns_type"
@confirm="onConfirm"
@cancel="showPicker = false"
/>
</van-popup>
</div>
</template>
<script setup>
import dayjs from "dayjs";
const props = defineProps({
item: Object,
});
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const showPicker = ref(false);
const currentTime = ref([]);
const readonly = props.item.component_props.readonly;
const onTap = () => {
if (readonly) return false; // 如果为只读,不能设置
showPicker.value = true
}
const onConfirm = ({ selectedValues, selectedOptions }) => {
props.item.value = selectedValues.join(":");
showPicker.value = false;
};
const columns_type = ref([]);
const date_format = props.item.component_props.data_dateformat; // HH:mm=时分,HH:mm:ss=时分秒
onMounted(() => {
// 根据默认值时间调整显示
currentTime.value = props.item.component_props.default ? props.item.component_props.default.split(":") : props.item.value.split(":");
let Hour = ''
let Minute = ''
let Second = ''
if (!props.item.component_props.default) {
Hour = String(dayjs().hour());
Minute = String(dayjs().minute());
Second = String(dayjs().second());
} else {
Hour = currentTime.value[0];
Minute = currentTime.value[1];
Second = currentTime.value[2];
}
switch (date_format) {
case "HH:mm":
columns_type.value = ['hour', 'minute'];
// 设置默认值
currentTime.value = [Hour, Minute];
break;
case "HH:mm:ss":
columns_type.value = ['hour', 'minute', 'second'];
// 设置默认值
currentTime.value = [Hour, Minute, Second];
break;
}
});
const required = props.item.component_props.required;
const data_minvalue = props.item.component_props.data_minvalue;
const data_maxvalue = props.item.component_props.data_maxvalue;
const validator = (val) => {
if (required && !val) {
return false;
} else if (val && data_minvalue && val < data_minvalue) {
return false;
} else if (val && data_maxvalue && val > data_maxvalue) {
return false;
} else {
return true;
}
};
// 错误提示文案
const validatorMessage = (val, rule) => {
if (required && !val) {
return "必填项不能为空";
} else if (val && data_minvalue && val < data_minvalue) {
return "最小可选:" + data_minvalue;
} else if (val && data_maxvalue && val > data_maxvalue) {
return "最大可选:" + data_maxvalue;
}
};
const rules = [{ validator, message: validatorMessage }];
</script>
<style lang="less" scoped>
.time-picker-field {
margin: 1rem;
.label {
// padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
font-weight: bold;
span {
color: red;
}
}
:deep(.van-icon) { // 处理正式服务器上箭头上下位移问题
font-size: var(--van-cell-icon-size);
line-height: var(--van-cell-line-height);
}
}
:deep(.van-cell--clickable) {
border: 1px solid #eaeaea;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
margin-top: 0.5rem;
input {
color: #323233;
}
}
</style>
<!--
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2022-05-23 13:42:35
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2022-11-22 17:12:52
* @FilePath: /data-table/src/components/VideoField/index.vue
* @Description:
-->
<template>
<div class="video-wrapper">
<div class="label">
{{ item.component_props.label }}
</div>
<div id="mui-player" class="video-div" />
</div>
</template>
<script setup>
/**
* 视频组件通用模块
*/
import "mui-player/dist/mui-player.min.css";
import MuiPlayer from "mui-player";
import { onMounted } from "vue";
import { useEventListener } from "@/composables";
// 视频基础属性
const props = defineProps({
item: Object,
});
// 视频播放事件回调
const emit = defineEmits(["active"]);
let video = null;
onMounted(() => {
const mp = new MuiPlayer({
container: "#mui-player",
// title: props.item.component_props.title,
src: props.item.component_props.src,
poster: props.item.component_props.cover,
autoFit: false,
videoAttribute: [
// 声明启用同层播放, 不让会自动全屏播放
{ attrKey: "webkit-playsinline", attrValue: "webkit-playsinline" },
{ attrKey: "playsinline", attrValue: "playsinline" },
{ attrKey: "x5-video-player-type", attrValue: "h5-page" },
],
});
video = mp.video();
//视频播放事件监听
video.addEventListener("play", function () {
props.item.value = { key: "video", value: "play" };
emit("active", props.item.value);
});
// 配置16:9高度比
const width = document.getElementById("mui-player").clientWidth;
const height = (width * 9) / 16;
document.getElementById("mui-player").height = height;
});
onUnmounted(() => {
video.removeEventListener("play", function () {});
});
</script>
<style lang="less" scoped>
.video-wrapper {
position: relative;
margin: 1rem 0;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.13);
.video-div {
// border-top-left-radius: 5px;
// border-top-right-radius: 5px;
border-radius: 5px;
}
.video-bar {
color: #713610;
padding: 1rem;
padding-bottom: 0.5rem;
}
}
:deep(.back-button) {
display: none !important;
}
</style>
This diff is collapsed. Click to expand it.
<!--
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2022-05-23 18:00:39
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2022-06-02 17:41:38
* @FilePath: /tswj/src/components/MuiVideo/test.vue
* @Description: 视频播放通用组件演示组件
* @Description: type: video 为纯视频播放框,bookDetail为定制模式
-->
<template>
<mui-video
v-for="(item, index) in mock"
:key="index"
:item="item"
type="video"
@on-play="onPlay"
/>
</template>
<script setup>
import mock from '@/components/MuiVideo/mock';
import MuiVideo from '@/components/MuiVideo/index';
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import {
Cookies,
$,
_,
axios,
storeToRefs,
mainStore,
Toast,
useTitle,
} from '@/utils/generatePackage.js';
//import { } from '@/utils/generateModules.js'
//import { } from '@/utils/generateIcons.js'
//import { } from '@/composables'
const $route = useRoute();
const $router = useRouter();
useTitle($route.meta.title);
const onPlay = ({ event, props }) => {
console.warn(event);
console.warn(props);
};
</script>
<style
lang="less"
scoped></style>
/**
* 依赖注入命名集合
*/
export const myInjectionKey = Symbol()
export const fooInjectionKey = Symbol()
import _ from '@/utils/lodash'
import TextField from '@/components/TextField/index.vue'
// import TextareaField from '@/components/TextareaField/index.vue'
// import RadioField from '@/components/RadioField/index.vue'
// import CheckboxField from '@/components/CheckboxField/index.vue'
// import PickerField from '@/components/PickerField/index.vue'
// import AreaPickerField from '@/components/AreaPickerField/index.vue'
// import DatePickerField from '@/components/DatePickerField/index.vue'
// import TimePickerField from '@/components/TimePickerField/index.vue'
// import DateTimePickerField from '@/components/DateTimePickerField/index.vue'
// import ImageUploaderField from '@/components/ImageUploaderField/index.vue'
// import FileUploaderField from '@/components/FileUploaderField/index.vue'
// import PhoneField from '@/components/PhoneField/index.vue'
// import EmailField from '@/components/EmailField/index.vue'
// import SignField from '@/components/SignField/index.vue'
// import RatePickerField from '@/components/RatePickerField/index.vue'
// import CalendarField from '@/components/CalendarField/index.vue'
// import IdentityField from '@/components/IdentityField/index.vue'
// import NumberField from '@/components/NumberField/index.vue'
// import DesField from '@/components/DesField/index.vue'
// import DividerField from '@/components/DividerField/index.vue'
// import VideoField from '@/components/VideoField/index.vue'
// import MarqueeField from '@/components/MarqueeField/index.vue'
// import ContactField from '@/components/ContactField/index.vue'
// import RuleField from '@/components/RuleField/index.vue'
// import MultiRuleField from '@/components/MultiRuleField/index.vue'
// import ButtonField from '@/components/ButtonField/index.vue'
// import NoteField from '@/components/NoteField/index.vue'
// import NameField from '@/components/NameField/index.vue'
// import GenderField from '@/components/GenderField/index.vue'
/**
* 生成自定义组件类型
* @param {*} data
* @type input 单行文本 TextField
* @type textarea 多行文本 TextareaField
* @type radio 单项选择 RadioField
* @type checkbox 多项选择 CheckboxField
* @type select 单列选择器 PickerField
* @type area_picker 地址选择器 AreaPickerField
* @type date_picker 日期选择器 DatePickerField
* @type time_picker 时间选择器 TimePickerField
* @type datetime_picker 日期时间选择器 DateTimePickerField
* @type image_uploader 图片上传 ImageUploaderField
* @type phone 手机输入框 PhoneField
* @type email 邮箱输入框 EmailField
* @type sign 电子签名输入框 SignField
* @type rate_picker 评分选择器 RatePickerField
* @type calendar 日历选择器 CalendarField
* @type id_code 身份证输入框 IdentityField
* @type desc 文字描述 DesField
* @type divider 分割线 DividerField
* @type video 视频控件 VideoField
* @type marquee 跑马灯控件 MarqueeField
* @type rule 活动规则控件 RuleField
* @type multi_rule 活动规则控件 MultiRuleField
* @type note 富文本控件 NoteField
* @type name 姓名控件 NameField
* @type gender 性别控件 GenderField
*/
export function createComponentType(data) {
// 判断类型和使用组件
_.each(data, (item, index) => {
// 必填项规则添加
if (item.component_props.required) {
item.rules = [
{
required: true,
message: item.placeholder ? item.placeholder : '必填项不能为空',
},
]
}
if (item.component_props.tag === 'input') {
item.type = 'text'
item.name = item.key
item.component = TextField
}
// if (item.component_props.tag === 'textarea') {
// item.type = 'textarea'
// item.name = item.key
// // item.rows = 10;
// item.autosize = true
// item.component = TextareaField
// }
// if (item.component_props.tag === 'number') {
// item.name = item.key
// item.component = NumberField
// }
// if (item.component_props.tag === 'radio') {
// item.component = RadioField
// }
// if (item.component_props.tag === 'checkbox') {
// item.component = CheckboxField
// }
// if (item.component_props.tag === 'select') {
// item.component = PickerField
// }
// if (item.component_props.tag === 'address') {
// item.component = AreaPickerField
// }
// if (item.component_props.tag === 'date') {
// item.component = DatePickerField
// }
// if (item.component_props.tag === 'time') {
// item.component = TimePickerField
// }
// if (item.component_props.tag === 'datetime') {
// item.component = DateTimePickerField
// }
// if (item.component_props.tag === 'image_uploader') {
// item.component = ImageUploaderField
// }
// if (item.component_props.tag === 'file_uploader') {
// item.component = FileUploaderField
// }
// if (item.component_props.tag === 'phone') {
// item.name = item.key
// item.component = PhoneField
// }
// if (item.component_props.tag === 'email') {
// item.name = item.key
// item.component = EmailField
// }
// if (item.component_props.tag === 'sign') {
// item.name = item.key
// item.component = SignField
// }
// if (item.component_props.tag === 'rate') {
// item.name = item.key
// item.component = RatePickerField
// }
// if (item.component_props.tag === 'calendar') {
// item.name = item.key
// item.component = CalendarField
// }
// if (item.component_props.tag === 'id_card') {
// item.name = item.key
// item.component = IdentityField
// }
// if (item.component_props.tag === 'desc') {
// item.name = item.key
// item.component = DesField
// }
// if (item.component_props.tag === 'divider') {
// item.name = item.key
// item.component = DividerField
// }
// if (item.component_props.tag === 'video') {
// item.name = item.key
// item.component = VideoField
// }
// if (item.component_props.tag === 'marquee') {
// item.name = item.key
// item.component = MarqueeField
// }
// if (item.component_props.tag === 'contact') {
// item.name = item.key
// item.component = ContactField
// }
// if (item.component_props.tag === 'rule') {
// item.name = item.key
// item.component = RuleField
// }
// if (item.component_props.tag === 'button') {
// item.name = item.key
// item.component = ButtonField
// }
// if (item.component_props.tag === 'multi_rule') {
// item.name = item.key
// item.value = []
// item.component = MultiRuleField
// }
// if (item.component_props.tag === 'note') {
// item.name = item.key
// item.component = NoteField
// }
// if (item.component_props.tag === 'name') {
// item.name = item.key
// item.component = NameField
// }
// if (item.component_props.tag === 'gender') {
// item.name = item.key
// item.component = GenderField
// }
})
}
import { provide, inject } from "vue";
// const key = Symbol();
/**
* 创建全局变量
* @param {*} context
* @param {*} key
*/
export function createContext(context, key) {
provide(key, context)
}
/**
* 使用全局变量
* @param {*} key
* @returns
*/
export function useContext(key) {
return inject(key)
}
/*
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2022-05-28 22:31:25
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2022-05-30 10:18:09
* @FilePath: /tswj/src/hooks/useDebounce.js
* @Description:
*/
import _ from 'lodash';
/**
* 封装lodash防抖
* @param {*} fn 执行函数
* @param {*} timestamp 执行间隔
* @param {*} options 函数配置 - 在延迟开始前调用,在延迟结束后不调用
* @returns 返回新的 debounced(防抖动)函数。
*/
export const useDebounce = (fn, timestamp = 500, options = { leading: true, trailing: false }) => {
return _.debounce(fn, timestamp, options);
}
/**
* @description 封装简化滚动查询列表执行流程
* @param {*} data 接口返回列表数据
* @param {*} list 自定义列表
* @param {*} offset
* @param {*} loading
* @param {*} finished
* @param {*} finishedTextStatus
* @param {*} emptyStatus
*/
import _ from 'lodash'
export const flowFn = (data, list, offset, loading, finished, finishedTextStatus, emptyStatus) => {
list.value = _.concat(list.value, data);
list.value = _.uniqBy(list.value, 'id');
offset.value = list.value.length;
loading.value = false;
// 数据全部加载完成
if (!data.length) {
// 加载状态结束
finished.value = true;
}
// 空数据提示
if (!list.value.length) {
finishedTextStatus.value = false;
}
emptyStatus.value = Object.is(list.value.length, 0);
}
/*
* @Date: 2022-07-21 13:28:05
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2022-08-22 09:51:52
* @FilePath: /front/src/hooks/useGo.js
* @Description: 文件描述
*/
import { useRouter } from 'vue-router';
import { getArticleAPI } from '@/api'
import { Cookies } from '@/utils/generatePackage'
import { parseQueryString } from '@/utils/tools'
/**
* 封装路由跳转方便行内调用
* @returns
*/
export function useGo () {
let router = useRouter()
function go (path, query) {
router.push({
path: path,
query: query
})
}
return go
}
/**
* 封装跳转文章详情页
* @param id 文章ID
* @param cid 栏目ID
* @param column 栏目名称
* @param section 二级栏目名称
* @param name 子栏目名称
* @returns
*/
export function useGoTo () {
let router = useRouter()
// function detail({ id, cid, column, section, name, post_link }) {
function detail(item) {
// 保存点击位置
// Cookies.set('scrollTop', getScrollTop());
// Cookies.set('scrollTopId', id);
// 判断是否跳转URL
// const { code, data } = await getArticleAPI({ f: 'article', i: id, cid });
// if (code) {
// if (data.post_link) { // 优先显示链接文章
// location.href = data.post_link;
// return false;
// }
// router.push({
// path: '/detail',
// query: { cid, id, column, section, name }
// });
// }
if (item) {
if (item.post_link) {
if (item.post_link.indexOf('f/guanzong/web')) {
item.post_link = item.post_link.replace('web', 'front');
}
if (item.post_link.indexOf('f/guanzong/web/#/list') > 0 || item.post_link.indexOf('f/guanzong/front/#/list') > 0) {
router.push({
path: '/list',
query: parseQueryString(item.post_link)
});
return false;
} else if (item.post_link) { // 优先显示链接文章
location.href = item.post_link;
return false;
}
} else {
router.push({
path: '/detail',
query: { cid: item.item, id: item.id, column: item.column, section: item.section, name: item.name }
});
}
}
}
return detail
}
export function useReplace () {
let router = useRouter()
function replace (path, query) {
router.replace({
path: path,
query: query
})
}
return replace
}
export function getScrollTop () {
var scroll_top = 0;
if (document.documentElement && document.documentElement.scrollTop) {
scroll_top = document.documentElement.scrollTop;
}
else if (document.body) {
scroll_top = document.body.scrollTop;
}
return scroll_top;
}
import { mainStore } from '@/utils/generatePackage.js'
// 删除 keep-alive 缓存
export const store = mainStore();
export const killPages = () => {
store.changeKeepPages();
}
export const addPages = () => {
store.keepThisPage();
}
<template>
<view class="index">
<view class="index-page">
<nut-config-provider :theme-vars="themeVars">
<nut-dialog no-cancel-btn title="温馨提示" content="表单收集量已达到限额,无法再提交数据。" v-model:visible="show_reach_sjsj_max_count" @ok="onOk" />
</nut-config-provider>
......@@ -23,9 +23,6 @@ const themeVars = {
primaryColor: styleColor.baseColor,
};
// web端判断
const is_pc = computed(() => process.env.TARO_ENV === 'h5' && wxInfo().isPC);
const code = getUrlParams(location.href) ? getUrlParams(location.href).code : '';
const model = getUrlParams(location.href) ? getUrlParams(location.href).model : '';
......
This diff is collapsed. Click to expand it.
......@@ -1633,6 +1633,11 @@
swiper "6.8.0"
weui "^1.1.2"
"@tarojs/extend@^3.6.2":
version "3.6.2"
resolved "https://mirrors.cloud.tencent.com/npm/@tarojs/extend/-/extend-3.6.2.tgz#f7badc3e322b76b616e10194f030cd5126ce4a6a"
integrity sha512-lKClhLFk9CPfT2z+djdzX4dBr3KLCzVystjC4KnZ00EAW7j6DlyiudwVUXmPwHAyEkq7n99qKZBy/lWQ3+p+cw==
"@tarojs/helper@3.6.2":
version "3.6.2"
resolved "https://mirrors.cloud.tencent.com/npm/@tarojs/helper/-/helper-3.6.2.tgz#f1f2e1225a8b3cdfa99ffd62349279350d2c01a2"
......