Quellcode durchsuchen

!1447 新增json编辑器组件,新增数字范围输入组件
Merge pull request !1447 from 摘繁华/next

zuohuaijun vor 1 Jahr
Ursprung
Commit
ad182eaff9

+ 13 - 3
Web/package.json

@@ -7,10 +7,19 @@
 	"author": "zuohuaijun",
 	"license": "MIT",
 	"scripts": {
-		"dev": "vite",
-		"build": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite build",
+		"bootstrap": "pnpm install",
+		"dev": "vite dev",
+		"build": "vite build",
+		"preview": "vite preview",
+		"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,vue,css,scss,less,json,md}\"",
+		"update-pkg": "ncu -u",
+		"cache-verify": "npm cache verify",
+		"clean-lock": "node ./scripts/clean-lock.js",
+		"clean-modules": "node ./scripts/clean-modules.js",
+		"clean-dist": "node ./scripts/clean-dist.js",
+		"clean-node": "pnpm run clean-lock && pnpm run clean-modules && pnpm run clean-dist",
+		"reinstall": "pnpm run clean-node && pnpm run bootstrap",
 		"lint-fix": "eslint --fix src/",
-		"format": "prettier --write .",
 		"build-api": "cd api_build/ && build.bat"
 	},
 	"dependencies": {
@@ -36,6 +45,7 @@
 		"ezuikit-js": "^8.1.1-alpha.2",
 		"js-cookie": "^3.0.5",
 		"js-table2excel": "^1.1.2",
+		"json-editor-vue": "^0.16.0",
 		"jsplumb": "^2.15.6",
 		"lodash-es": "^4.17.21",
 		"md-editor-v3": "^4.21.2",

+ 16 - 0
Web/scripts/clean-dist.js

@@ -0,0 +1,16 @@
+// clean.js
+import fs from 'fs';
+import path from 'path';
+
+const directoriesToDelete = ['dist'];
+
+// 删除文件夹
+directoriesToDelete.forEach((dir) => {
+	const dirPath = path.join(process.cwd(), dir);
+	if (fs.existsSync(dirPath)) {
+		fs.rmSync(dirPath, { recursive: true, force: true });
+		console.log(`Deleted directory: ${dirPath}`);
+	} else {
+		console.log(`Directory not found: ${dirPath}`);
+	}
+});

+ 16 - 0
Web/scripts/clean-lock.js

@@ -0,0 +1,16 @@
+// clean.js
+import fs from "fs";
+import path from "path";
+
+const filesToDelete = ["pnpm-lock.yaml", "package-lock.json"];
+
+// 删除文件
+filesToDelete.forEach(file => {
+  const filePath = path.join(process.cwd(), file);
+  if (fs.existsSync(filePath)) {
+    fs.unlinkSync(filePath);
+    console.log(`Deleted file: ${filePath}`);
+  } else {
+    console.log(`File not found: ${filePath}`);
+  }
+});

+ 16 - 0
Web/scripts/clean-modules.js

@@ -0,0 +1,16 @@
+// clean.js
+import fs from 'fs';
+import path from 'path';
+
+const directoriesToDelete = ['node_modules'];
+
+// 删除文件夹
+directoriesToDelete.forEach((dir) => {
+	const dirPath = path.join(process.cwd(), dir);
+	if (fs.existsSync(dirPath)) {
+		fs.rmSync(dirPath, { recursive: true, force: true });
+		console.log(`Deleted directory: ${dirPath}`);
+	} else {
+		console.log(`Directory not found: ${dirPath}`);
+	}
+});

+ 64 - 0
Web/src/components/jsonEditor/index.vue

@@ -0,0 +1,64 @@
+<!--
+// JsonEditor组件,用于编辑类json格式数据,防止配置式数据的错误格式输入
+// 使用示例:
+<template>
+	<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
+		<el-form-item label="内容" prop="content">
+			<JsonEditor ref="jsonEditorRef" v-model:jsonObj="ruleForm.content"></JsonEditor>
+		</el-form-item>
+	</el-col>
+</template>
+<script lang="ts" setup>
+import JsonEditor from '/@/components/jsonEditor/index.vue';
+</script>
+-->
+
+<template>
+	<!-- 根据 isJsonValid 控制 JsonEditorVue 的显示 -->
+	<JsonEditorVue ref="jsonEditorVueRef" v-show="isJsonValid" v-model="internalJsonObj" mode="text" style="width: 100%" />
+</template>
+
+<script setup lang="ts" name="jsonEditor">
+import { ref, useTemplateRef, watch, onMounted, computed } from 'vue';
+import JsonEditorVue from 'json-editor-vue';
+
+const props = defineProps({
+	jsonObj: {
+		type: null,
+		default: null, // 允许为 null
+		required: false, // 不是必需的
+	},
+});
+const jsonEditorVueRef = useTemplateRef('jsonEditorVueRef');
+const internalJsonObj = ref(props.jsonObj);
+const emit = defineEmits(['update:jsonObj']);
+
+// 计算属性,判断 jsonObj 是否为有效的 JSON 字符串,这里为简易判断,防止输入框失去焦点时,组件频繁隐藏和显示
+const isJsonValid = computed(() => {
+	try {
+		if (internalJsonObj.value && (internalJsonObj.value.startsWith('{') || internalJsonObj.value.startsWith('[')) && (internalJsonObj.value.endsWith('}') || internalJsonObj.value.endsWith(']'))) {
+			return true;
+		}
+	} catch {
+		return false;
+	}
+	return false;
+});
+
+// 监听内部 JSON 对象变化并触发外部更新
+watch(internalJsonObj, () => {
+	emit('update:jsonObj', internalJsonObj.value);
+});
+
+// 监听外部字符串变化并更新内部JSON对象
+watch(
+	() => props.jsonObj,
+	(newVal) => {
+		internalJsonObj.value = newVal;
+	}
+);
+
+onMounted(() => {
+	jsonEditorVueRef.value.jsonEditor.focus();
+});
+</script>

+ 346 - 0
Web/src/components/numberRange/index.vue

@@ -0,0 +1,346 @@
+<!-- 
+// NumberRange组件,用于输入数字范围(支持前缀和后缀插槽,支持数字精度,支持限制取值范围),在数值、金额等的范围输入场景中使用
+// 使用示例:
+<template>
+	<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="4" class="mb10">
+		<el-form-item label="订单金额">
+			<number-range v-model="queryParams.amountRange">
+				<template #prepend>
+					<span>范围</span>
+				</template>
+			</number-range>
+		</el-form-item>
+	</el-col>
+</template>
+<script lang="ts" setup>
+import NumberRange from '/@/components/numberRange/index.vue';
+</script>
+-->
+
+<template>
+	<div class="number-range-container">
+		<div :id="usePrepend ? 'prepend' : ''" :class="{ 'slot-default': slotStyle === 'default', 'slot-pend ': usePrepend }">
+			<slot name="prepend">
+				<!-- 前缀插槽 -->
+			</slot>
+		</div>
+		<div
+			class="number-range"
+			:class="{
+				'is-disabled': disabled,
+				'is-focus': isFocus,
+				'number-range-left-border-radius-0': usePrepend,
+				'number-range-right-border-radius-0': useAppend,
+			}"
+		>
+			<el-input-number
+				:disabled="disabled"
+				placeholder="最小值"
+				@blur="handleBlur"
+				@focus="handleFocus"
+				@change="handleChangeMinValue"
+				@update:modelValue="updateMinValue"
+				v-model="minValue_"
+				v-bind="$attrs"
+				:controls="false"
+			/>
+			<div class="to">
+				<span>{{ to }}</span>
+			</div>
+			<el-input-number
+				:disabled="disabled"
+				placeholder="最大值"
+				@blur="handleBlur"
+				@focus="handleFocus"
+				@change="handleChangeMaxValue"
+				@update:modelValue="updateMaxValue"
+				v-model="maxValue_"
+				v-bind="$attrs"
+				:controls="false"
+			/>
+		</div>
+		<div :id="useAppend ? 'append' : ''" :class="{ 'slot-default': slotStyle === 'default', 'slot-pend ': useAppend }">
+			<slot name="append">
+				<!-- 后缀插槽 -->
+			</slot>
+		</div>
+	</div>
+</template>
+<script lang="ts" setup name="numberRange">
+import { computed, ref, useSlots } from 'vue';
+
+const props = defineProps({
+	modelValue: {
+		type: Array<Number>,
+		default: () => [null, null], // 调用时使用v-model="[min,max]" 绑定
+	},
+	minValue: {
+		type: Number,
+		default: null, // 调用时使用v-model:min-value="" 绑定多个v-model
+	},
+	maxValue: {
+		type: Number,
+		default: null, // 调用时使用v-model:max-value="" 绑定多个v-model
+	},
+	// 是否禁用
+	disabled: {
+		type: Boolean,
+		default: false,
+	},
+	to: {
+		type: String,
+		default: '至',
+	},
+	// 精度参数 -保留小数位数
+	precision: {
+		type: Number,
+		default: 0,
+		validator(val: number) {
+			return val >= 0 && val === parseInt(String(val), 10);
+		},
+	},
+	// 限制取值范围
+	valueRange: {
+		type: Array,
+		default: () => [],
+		validator(val: []) {
+			if (val && val.length > 0) {
+				// @ts-ignore
+				if (val.length !== 2) {
+					throw new Error('请传入长度为2的Number数组');
+				}
+				// @ts-ignore
+				if (typeof val[0] !== 'number' || typeof val[1] !== 'number') {
+					throw new Error('取值范围只接受Number类型,请确认');
+				}
+				// @ts-ignore
+				if (val[1] < val[0]) {
+					throw new Error('valueRange格式须为[最小值,最大值],请确认');
+				}
+			}
+			return true;
+		},
+	},
+	// 插槽样式
+	slotStyle: {
+		type: String, // default --异色背景 |  plain--无背景色
+		default: 'default',
+	},
+});
+
+const emit = defineEmits(['update:modelValue', 'update:minValue', 'update:maxValue', 'change']);
+
+const minValue_ = computed({
+	get() {
+		return props.minValue || props.modelValue[0] || null;
+	},
+	set(value) {
+		emit('update:minValue', value);
+		emit('update:modelValue', [value, maxValue_.value]);
+	},
+});
+
+const maxValue_ = computed({
+	get() {
+		return props.maxValue || props.modelValue[1] || null;
+	},
+	set(value) {
+		emit('update:maxValue', value);
+		emit('update:modelValue', [minValue_.value, value]);
+	},
+});
+
+const handleChangeMinValue = (value: number) => {
+	// 非数字空返回null
+	if (isNaN(value)) {
+		emit('update:minValue', null);
+		return;
+	}
+	// 初始化数字精度
+	const newMinValue = parsePrecision(value, props.precision);
+	// min > max 交换min max
+	if (typeof newMinValue === 'number' && parseFloat(String(newMinValue)) > parseFloat(String(maxValue_.value))) {
+		// 取值范围判定
+		const { min, max } = decideValueRange(Number(maxValue_.value), newMinValue);
+		// 更新绑定值
+		updateValue(min, max);
+	} else {
+		// 取值范围判定
+		const { min, max } = decideValueRange(newMinValue, Number(maxValue_.value));
+		// 更新绑定值
+		updateValue(min, max);
+	}
+};
+
+const handleChangeMaxValue = (value: number) => {
+	// 非数字空返回null
+	if (isNaN(value)) {
+		emit('update:maxValue', null);
+		return;
+	}
+	// 初始化数字精度
+	const newMaxValue = parsePrecision(value, props.precision);
+	// max < min 交换min max
+	if (typeof newMaxValue === 'number' && parseFloat(String(newMaxValue)) < parseFloat(String(minValue_.value))) {
+		// 取值范围判定
+		const { min, max } = decideValueRange(newMaxValue, Number(minValue_.value));
+		// 更新绑定值
+		updateValue(min, max);
+	} else {
+		// 取值范围判定
+		const { min, max } = decideValueRange(Number(minValue_.value), newMaxValue);
+		// 更新绑定值
+		updateValue(min, max);
+	}
+};
+
+const updateMinValue = (value: number) => {
+	minValue_.value = value;
+};
+
+const updateMaxValue = (value: number) => {
+	maxValue_.value = value;
+};
+
+// 更新数据
+const updateValue = (min: number, max: number) => {
+	emit('update:minValue', min);
+	emit('update:maxValue', max);
+	emit('update:modelValue', [min, max]);
+	emit('change', { min, max });
+};
+
+// 取值范围判定
+const decideValueRange = (min: number, max: number) => {
+	if (props.valueRange && props.valueRange.length > 0) {
+		// @ts-ignore
+		min = min < props.valueRange[0] ? props.valueRange[0] : min > props.valueRange[1] ? props.valueRange[1] : min;
+		// @ts-ignore
+		max = max > props.valueRange[1] ? props.valueRange[1] : max;
+	}
+	return { min, max };
+};
+
+// input焦点事件
+const isFocus = ref();
+
+const handleFocus = () => {
+	isFocus.value = true;
+};
+
+const handleBlur = () => {
+	isFocus.value = false;
+};
+
+// 处理数字精度
+const parsePrecision = (number: number, precision = 0) => {
+	return parseFloat(String(Math.round(number * Math.pow(10, precision)) / Math.pow(10, precision)));
+};
+
+// 判断插槽是否被使用
+// 组件外部使用时插入了
+// <template #插槽名 >
+// </template>
+// 无论template标签内是否插入了内容,均视为已使用该插槽
+const slots = useSlots();
+const usePrepend = computed(() => {
+	// 前缀插槽
+	return slots && slots.prepend ? true : false;
+});
+const useAppend = computed(() => {
+	// 后缀插槽
+	return slots && slots.append ? true : false;
+});
+</script>
+<style lang="scss" scoped>
+.number-range-container {
+	display: flex;
+	height: 100%;
+	.slot-pend {
+		white-space: nowrap;
+		color: var(--el-color-info);
+		border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
+	}
+	#prepend {
+		padding: 0 20px;
+		box-shadow:
+			1px 0 0 0 var(--el-input-border-color, var(--el-border-color)) inset,
+			0 1px 0 0 var(--el-input-border-color, var(--el-border-color)) inset,
+			0 -1px 0 0 var(--el-input-border-color, var(--el-border-color)) inset;
+		border-right: 0;
+		border-top-right-radius: 0;
+		border-bottom-right-radius: 0;
+	}
+	#append {
+		padding: 0 15px;
+		box-shadow:
+			0 1px 0 0 var(--el-input-border-color, var(--el-border-color)) inset,
+			0 -1px 0 0 var(--el-input-border-color, var(--el-border-color)) inset,
+			-1px 0 0 0 var(--el-input-border-color, var(--el-border-color)) inset;
+		border-left: 0;
+		border-top-left-radius: 0;
+		border-bottom-left-radius: 0;
+	}
+	.slot-default {
+		background-color: var(--el-fill-color-light);
+	}
+
+	.number-range-left-border-radius-0 {
+		border-top-left-radius: 0 !important;
+		border-bottom-left-radius: 0 !important;
+	}
+	.number-range-right-border-radius-0 {
+		border-top-right-radius: 0 !important;
+		border-bottom-right-radius: 0 !important;
+	}
+
+	.number-range {
+		background-color: var(--el-bg-color) !important;
+		box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
+		border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
+		padding: 0 2px;
+		display: flex;
+		flex-direction: row;
+		width: 100%;
+		justify-content: center;
+		align-items: center;
+		color: var(--el-input-text-color, var(--el-text-color-regular));
+		transition: var(--el-transition-box-shadow);
+		transform: translate3d(0, 0, 0);
+		overflow: hidden;
+
+		.to {
+			margin-top: 1px;
+		}
+	}
+
+	.is-focus {
+		transition: all 0.3s;
+		box-shadow: 0 0 0 1px var(--el-color-primary) inset !important;
+	}
+	.is-disabled {
+		background-color: var(--el-input-bg-color);
+		color: var(--el-input-text-color, var(--el-text-color-regular));
+		cursor: not-allowed;
+		.to {
+			height: calc(100% - 3px);
+			background-color: var(--el-fill-color-light) !important;
+		}
+	}
+}
+
+:deep(.el-input) {
+	border: none;
+}
+:deep(.el-input__wrapper) {
+	margin: 0;
+	padding: 0 15px;
+	background-color: transparent;
+	border: none !important;
+	box-shadow: none !important;
+	&.is-focus {
+		border: none !important;
+		box-shadow: none !important;
+	}
+}
+</style>