fix:remove ElInput

This commit is contained in:
zyronon
2025-08-16 23:28:31 +08:00
parent aa22d21ff8
commit fd4fcf5473
8 changed files with 352 additions and 182 deletions

View File

@@ -25,7 +25,7 @@
--shadow: rgba(0, 0, 0, 0.08) 0px 4px 12px;
--panel-margin-left: calc(50% + var(--toolbar-width) / 2 + 1rem);
--article-panel-margin-left: calc(50% + var(--article-width) / 2 + 1rem);
--anim-time: 0.3s;
--anim-time: 0.5s;
--color-input-color: black;
--color-input-bg: white;
@@ -409,8 +409,8 @@ a {
background: var(--color-second);
}
.center {
@apply flex justify-center items-center;
.inline-center {
@apply inline-flex justify-center items-center;
}
.title {

View File

@@ -14,13 +14,13 @@ import {saveAs} from "file-saver";
import {GITHUB} from "@/config/ENV.ts";
import dayjs from "dayjs";
import BasePage from "@/pages/pc/components/BasePage.vue";
import {ElInputNumber} from 'element-plus'
import Toast from '@/pages/pc/components/base/toast/Toast.ts'
import {Option, Select} from "@/pages/pc/components/base/select";
import Switch from "@/pages/pc/components/base/Switch.vue";
import Slider from "@/pages/pc/components/base/Slider.vue";
import RadioGroup from "@/pages/pc/components/base/radio/RadioGroup.vue";
import Radio from "@/pages/pc/components/base/radio/Radio.vue";
import InputNumber from "@/pages/pc/components/base/InputNumber.vue";
const emit = defineEmits<{
toggleDisabledDialogEscKey: [val: boolean]
@@ -284,7 +284,7 @@ function importData(e) {
</RadioGroup>
<div class="mini-row" v-if="settingStore.repeatCount === 100">
<label class="item-title">循环次数</label>
<ElInputNumber v-model="settingStore.repeatCustomCount"
<InputNumber v-model="settingStore.repeatCustomCount"
:min="6"
:max="15"
type="number"
@@ -365,10 +365,10 @@ function importData(e) {
<div class="row">
<label class="sub-title">切换下一个单词时间</label>
<div class="wrapper">
<ElInputNumber v-model="settingStore.waitTimeForChangeWord"
:min="6"
:max="100"
type="number"
<InputNumber v-model="settingStore.waitTimeForChangeWord"
:min="10"
:max="100"
type="number"
/>
<span>毫秒</span>
</div>

View File

@@ -9,7 +9,6 @@ import {genArticleSectionData, splitCNArticle2, splitEnArticle2, usePlaySentence
import {_nextTick, _parseLRC, cloneDeep, last} from "@/utils";
import {watch} from "vue";
import Empty from "@/components/Empty.vue";
import {ElInputNumber} from "element-plus";
import Toast from '@/pages/pc/components/base/toast/Toast.ts'
import * as Comparison from "string-comparison"
import BaseIcon from "@/components/BaseIcon.vue";
@@ -18,6 +17,7 @@ import {getDefaultArticle} from "@/types/func.ts";
import copy from "copy-to-clipboard";
import {Option, Select} from "@/pages/pc/components/base/select";
import Tooltip from "@/pages/pc/components/base/Tooltip.vue";
import InputNumber from "@/pages/pc/components/base/InputNumber.vue";
interface IProps {
article?: Article,
@@ -497,11 +497,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
<div>开始时间</div>
<div class="flex justify-between flex-1">
<div class="flex items-center gap-2">
<ElInputNumber v-model="editSentence.audioPosition[0]" :precision="2" :step="0.1">
<template #suffix>
<span>s</span>
</template>
</ElInputNumber>
<InputNumber v-model="editSentence.audioPosition[0]" :precision="2" :step="0.1"/>
<BaseIcon
@click="jumpAudio(editSentence.audioPosition[0])"
title="跳转"
@@ -520,11 +516,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
<div>结束时间</div>
<div class="flex justify-between flex-1">
<div class="flex items-center gap-2">
<ElInputNumber v-model="editSentence.audioPosition[1]" :precision="2" :step="0.1">
<template #suffix>
<span>s</span>
</template>
</ElInputNumber>
<InputNumber v-model="editSentence.audioPosition[1]" :precision="2" :step="0.1"/>
<span></span>
<BaseButton size="small" @click="editSentence.audioPosition[1] = -1">结束</BaseButton>
</div>

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import BaseButton from "@/components/BaseButton.vue";
import {ElInput} from "element-plus";
import {watchEffect} from "vue";
import BaseInput from "@/pages/pc/components/base/BaseInput.vue";
import Textarea from "@/pages/pc/components/base/Textarea.vue";
interface IProps {
value: string,
@@ -32,6 +32,7 @@ function save() {
function toggle() {
edit = !edit
editVal = props.value
}
</script>
@@ -39,7 +40,7 @@ function toggle() {
<div
v-if="edit"
class="edit-text">
<BaseInput
<Textarea
v-model="editVal"
ref="inputRef"
textarea
@@ -48,7 +49,7 @@ function toggle() {
type="textarea"
:input-style="`color: var(--color-font-1);font-size: 1rem;`"
/>
<div class="options">
<div class="flex justify-end mt-2">
<BaseButton @click="toggle">取消</BaseButton>
<BaseButton @click="save">应用</BaseButton>
</div>
@@ -65,13 +66,6 @@ function toggle() {
.edit-text {
margin-top: .6rem;
color: var(--color-font-1);
.options {
margin-top: .6rem;
gap: .6rem;
display: flex;
justify-content: flex-end;
}
}
.text {

View File

@@ -1,10 +1,5 @@
<script setup lang="ts">
import {ref, computed, watch, defineProps, defineEmits, useAttrs, nextTick, PropType} from 'vue';
interface Autosize {
minRows?: number;
maxRows?: number;
}
import {ref, watch, defineProps, defineEmits, useAttrs} from 'vue';
const props = defineProps({
modelValue: [String, Number],
@@ -18,28 +13,18 @@ const props = defineProps({
type: Boolean,
default: false,
},
textarea: {
type: Boolean,
default: false,
},
required: {
type: Boolean,
default: false,
},
maxLength: Number,
autosize: {
type: [Boolean, Object] as PropType<boolean | Autosize>,
default: false,
},
});
const emit = defineEmits(['update:modelValue', 'input', 'change', 'focus', 'blur', 'validation']);
const attrs = useAttrs();
const inputValue = ref(props.modelValue);
const errorMsg = ref('');
const textareaRef = ref<HTMLTextAreaElement | null>(null);
watch(() => props.modelValue, (val) => {
inputValue.value = val;
@@ -60,17 +45,11 @@ const validate = (val: string | number | null | undefined) => {
};
const onInput = (e: Event) => {
const target = e.target as HTMLInputElement | HTMLTextAreaElement;
const target = e.target as HTMLInputElement;
inputValue.value = target.value;
validate(target.value);
emit('update:modelValue', target.value);
emit('input', e);
if (props.textarea && props.autosize) {
nextTick(() => {
calcTextareaHeight();
});
}
};
const onChange = (e: Event) => {
@@ -90,105 +69,32 @@ const clearInput = () => {
inputValue.value = '';
validate('');
emit('update:modelValue', '');
if (props.textarea && props.autosize) {
nextTick(() => {
calcTextareaHeight();
});
}
};
// 计算并设置 textarea 高度,支持 autosize 功能
const calcTextareaHeight = () => {
if (!textareaRef.value) return;
const ta = textareaRef.value;
ta.style.height = 'auto'; // 先重置高度
const style = window.getComputedStyle(ta);
const lineHeight = parseFloat(style.lineHeight);
const paddingTop = parseFloat(style.paddingTop);
const paddingBottom = parseFloat(style.paddingBottom);
let height = ta.scrollHeight;
let minRows = 1;
let maxRows = Infinity;
if (typeof props.autosize === 'object') {
if (props.autosize.minRows) minRows = props.autosize.minRows;
if (props.autosize.maxRows) maxRows = props.autosize.maxRows;
} else if (props.autosize === true) {
minRows = 1;
maxRows = Infinity;
}
const minHeight = lineHeight * minRows + paddingTop + paddingBottom;
const maxHeight = lineHeight * maxRows + paddingTop + paddingBottom;
height = Math.min(Math.max(height, minHeight), maxHeight);
ta.style.height = height + 'px';
};
// 组件初始化时,调整高度(针对多行)
if (props.textarea && props.autosize) {
nextTick(() => {
calcTextareaHeight();
});
}
</script>
<template>
<div class="custom-input" :class="{ 'is-disabled': disabled, 'has-error': errorMsg }">
<template v-if="textarea">
<textarea
v-bind="attrs"
ref="textareaRef"
:placeholder="placeholder"
:disabled="disabled"
:value="inputValue"
@input="onInput"
@change="onChange"
@focus="onFocus"
@blur="onBlur"
class="custom-input__textarea"
:maxlength="maxLength"
rows="1"
:style="autosize ? {overflowY: 'hidden'} : {}"
></textarea>
<button
v-if="clearable && inputValue && !disabled"
type="button"
class="custom-input__clear"
@click="clearInput"
aria-label="Clear input"
>×
</button>
</template>
<template v-else>
<input
v-bind="attrs"
:type="type"
:placeholder="placeholder"
:disabled="disabled"
:value="inputValue"
@input="onInput"
@change="onChange"
@focus="onFocus"
@blur="onBlur"
class="custom-input__inner"
:maxlength="maxLength"
/>
<button
v-if="clearable && inputValue && !disabled"
type="button"
class="custom-input__clear"
@click="clearInput"
aria-label="Clear input"
>×
</button>
</template>
<input
v-bind="attrs"
:type="type"
:placeholder="placeholder"
:disabled="disabled"
:value="inputValue"
@input="onInput"
@change="onChange"
@focus="onFocus"
@blur="onBlur"
class="custom-input__inner"
:maxlength="maxLength"
/>
<button
v-if="clearable && inputValue && !disabled"
type="button"
class="custom-input__clear"
@click="clearInput"
aria-label="Clear input"
>×
</button>
<div v-if="errorMsg" class="custom-input__error">{{ errorMsg }}</div>
</div>
@@ -205,8 +111,7 @@ if (props.textarea && props.autosize) {
}
&.has-error {
.custom-input__inner,
.custom-input__textarea {
.custom-input__inner {
border-color: #f56c6c;
}
@@ -217,15 +122,13 @@ if (props.textarea && props.autosize) {
}
}
&__inner,
&__textarea {
&__inner {
width: 100%;
padding: 0.4rem 1.5rem 0.4rem 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
resize: vertical;
transition: all .3s;
color: var(--color-input-color);
background: var(--color-input-bg);
@@ -242,11 +145,6 @@ if (props.textarea && props.autosize) {
}
}
&__textarea {
min-height: 5rem;
overflow-y: auto;
}
&__clear {
position: absolute;
right: 0.4rem;
@@ -270,9 +168,4 @@ if (props.textarea && props.autosize) {
padding-left: 0.5rem;
}
}
.custom-input__textarea {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<div class="input-number inline-center select-none anim" :class="{ 'is-disabled': disabled }">
<!-- 减号 -->
<button
class="btn minus-btn inline-center cursor-pointer anim border-none outline-none w-8 h-8"
type="button"
:disabled="disabled || isMin"
@mousedown.prevent="onHold(-1)"
@mouseup="onRelease"
@mouseleave="onRelease"
aria-label="decrease"
>-
</button>
<!-- 输入框 -->
<input
ref="inputRef"
class="flex-1 h-8 px-2 text-center border-none outline-none bg-transparent input-inner w-14"
:value="displayValue"
:disabled="disabled"
inputmode="decimal"
@input="e => displayValue = e.target.value"
@keydown.up.prevent="change(1)"
@keydown.down.prevent="change(-1)"
@blur="onBlur"
/>
<!-- 加号 -->
<button
class="btn plus-btn inline-center cursor-pointer anim border-none outline-none w-8 h-8"
type="button"
:disabled="disabled || isMax"
@mousedown.prevent="onHold(1)"
@mouseup="onRelease"
@mouseleave="onRelease"
aria-label="increase"
>+
</button>
</div>
</template>
<script setup lang="ts">
import {ref, computed, onBeforeUnmount} from 'vue'
const props = defineProps({
modelValue: {type: [Number, String], default: null},
min: {type: Number, default: -Infinity},
max: {type: Number, default: Infinity},
step: {type: Number, default: 1},
precision: {type: Number},
disabled: {type: Boolean, default: false},
stepStrictly: {type: Boolean, default: false},
})
const emit = defineEmits(['update:modelValue', 'input', 'change'])
const inputRef = ref<HTMLInputElement | null>(null)
const inner = ref<number | null>(normalizeToNumber(props.modelValue))
let holdTimer: number | null = null
let holdInterval: number | null = null
const displayValue = computed({
get: () => inner.value === null ? '' : format(inner.value),
set: v => {
const n = parseInput(v)
if (n === 'editing') return
setValue(n)
}
})
const isMin = computed(() => inner.value !== null && inner.value <= props.min)
const isMax = computed(() => inner.value !== null && inner.value >= props.max)
function normalizeToNumber(v: any): number | null {
const n = Number(v)
return Number.isFinite(n) ? n : null
}
function clamp(n: number | null) {
if (n === null) return null
if (n < props.min) return props.min
if (n > props.max) return props.max
return n
}
function format(n: number) {
return props.precision != null ? n.toFixed(props.precision) : String(n)
}
function parseInput(s: string): number | 'editing' | null {
const trimmed = s.trim()
if (['', '-', '+', '.', '-.', '+.'].includes(trimmed)) return 'editing'
const n = Number(trimmed)
return Number.isFinite(n) ? n : 'editing'
}
function applyStepStrict(n: number | null) {
if (n === null) return null
if (!props.stepStrictly) return n
const base = Number.isFinite(props.min) ? props.min : 0
const k = Math.round((n - base) / props.step)
return base + k * props.step
}
function toPrecision(n: number) {
return props.precision != null ? Number(n.toFixed(props.precision)) : n
}
function setValue(n: number | null) {
const v = clamp(toPrecision(applyStepStrict(n)))
inner.value = v
emit('update:modelValue', v)
emit('input', v)
emit('change', v)
}
function change(dir: 1 | -1) {
if (props.disabled) return
const base = inner.value ?? (Number.isFinite(props.min) ? props.min : 0)
setValue(base + dir * props.step)
}
function onHold(dir: 1 | -1) {
change(dir)
holdTimer = window.setTimeout(() => {
holdInterval = window.setInterval(() => change(dir), 100)
}, 400)
}
function onRelease() {
if (holdTimer) {
clearTimeout(holdTimer);
holdTimer = null
}
if (holdInterval) {
clearInterval(holdInterval);
holdInterval = null
}
}
function onBlur() {
const n = parseInput(displayValue.value)
setValue(n === 'editing' ? inner.value : n)
}
onBeforeUnmount(onRelease)
</script>
<style scoped lang="scss">
.input-number {
border: 1px solid var(--color-input-border);
overflow: hidden;
border-radius: 4px;
background: var(--color-input-bg);
&:hover {
border-color: var(--color-select-bg);
}
&.is-disabled {
opacity: .7;
.btn, .input-inner {
cursor: not-allowed;
}
}
.input-inner {
color: var(--color-input-color);
}
.btn {
background: var(--color-second);
color: var(--color-input-color);
&.minus-btn {
border-right: 1px solid var(--color-input-border);
}
&.plus-btn {
border-left: 1px solid var(--color-input-border);
}
&:hover {
background: var(--color-third);
color: var(--color-select-bg);
}
&:disabled {
opacity: .5;
cursor: not-allowed;
}
}
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<div class="inline-flex w-full relative">
<textarea
ref="textareaRef"
v-model="innerValue"
:placeholder="placeholder"
:maxlength="maxlength"
:rows="rows"
:style="textareaStyle"
class="w-full px-3 py-2 border border-gray-300 rounded-md outline-none resize-none transition-colors duration-200 box-border"
@input="handleInput"
/>
<!-- 字数统计 -->
<span
v-if="showWordLimit && maxlength"
class="absolute bottom-1 right-2 text-xs text-gray-400 select-none"
>
{{ innerValue.length }} / {{ maxlength }}
</span>
</div>
</template>
<script setup lang="ts">
import {ref, watch, computed, nextTick} from "vue"
const props = defineProps({
modelValue: String,
placeholder: String,
maxlength: Number,
rows: {type: Number, default: 1},
autosize: {
type: [Boolean, Object] as () => boolean | { minRows?: number; maxRows?: number },
default: false
},
showWordLimit: Boolean
})
const emit = defineEmits(["update:modelValue"])
const innerValue = ref(props.modelValue ?? "")
watch(() => props.modelValue, v => (innerValue.value = v ?? ""))
const textareaRef = ref<HTMLTextAreaElement>()
// 样式(用于控制高度)
const textareaStyle = computed(() => {
return props.autosize ? { height: "auto" } : {}
})
// 输入处理
const handleInput = (e: Event) => {
const val = (e.target as HTMLTextAreaElement).value
innerValue.value = val
emit("update:modelValue", val)
if (props.autosize) nextTick(resizeTextarea)
}
// 自动调整高度
const resizeTextarea = () => {
if (!textareaRef.value) return
const el = textareaRef.value
el.style.height = "auto"
let height = el.scrollHeight
let overflow = "hidden"
if (typeof props.autosize === "object") {
const { minRows, maxRows } = props.autosize
const lineHeight = 24 // 行高约等于 24px
if (minRows) height = Math.max(height, minRows * lineHeight)
if (maxRows) {
const maxHeight = maxRows * lineHeight
if (height > maxHeight) {
height = maxHeight
overflow = "auto" // 超出时允许滚动
}
}
}
el.style.height = height + "px"
el.style.overflowY = overflow
}
watch(innerValue, () => {
if (props.autosize) nextTick(resizeTextarea)
}, {immediate: true})
</script>
<style>
textarea {
font-family: var(--font-family);
color: var(--color-input-color);
background: var(--color-input-bg);
&:focus {
outline: none;
border-color: #409eff;
box-shadow: 0 0 3px #409eff;
}
}
</style>

View File

@@ -10,7 +10,7 @@ import BaseIcon from "@/components/BaseIcon.vue";
import BaseTable from "@/pages/pc/components/BaseTable.vue";
import WordItem from "@/pages/pc/components/WordItem.vue";
import type {FormInstance, FormRules} from "element-plus";
import {ElForm, ElFormItem, ElInput} from "element-plus";
import {ElForm, ElFormItem} from "element-plus";
import Toast from '@/pages/pc/components/base/toast/Toast.ts'
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
import BackIcon from "@/pages/pc/components/BackIcon.vue";
@@ -20,6 +20,7 @@ import {useBaseStore} from "@/stores/base.ts";
import EditBook from "@/pages/pc/article/components/EditBook.vue";
import {getDefaultDict} from "@/types/func.ts";
import BaseInput from "@/pages/pc/components/base/BaseInput.vue";
import Textarea from "@/pages/pc/components/base/Textarea.vue";
const runtimeStore = useRuntimeStore()
const base = useBaseStore()
@@ -208,7 +209,8 @@ defineRender(() => {
<BackIcon class="z-2" onClick={() => router.back()}/>
<div class="absolute page-title text-align-center w-full">{runtimeStore.editDict.name}</div>
<div class="flex">
<BaseButton loading={studyLoading || loading} type="info" onClick={() => isEdit = true}>编辑</BaseButton>
<BaseButton loading={studyLoading || loading} type="info"
onClick={() => isEdit = true}>编辑</BaseButton>
<BaseButton loading={studyLoading || loading} onClick={addMyStudyList}>学习</BaseButton>
</div>
</div>
@@ -286,52 +288,46 @@ defineRender(() => {
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
</ElFormItem>
<ElFormItem label="翻译">
<ElInput
<Textarea
modelValue={wordForm.trans}
onUpdate:modelValue={e => wordForm.trans = e}
placeholder="一行一个翻译前面词性后面内容如n.取消);多个翻译请换行"
autosize={{minRows: 6, maxRows: 10}}
type="textarea"/>
autosize={{minRows: 6, maxRows: 10}}/>
</ElFormItem>
<ElFormItem label="例句">
<ElInput
<Textarea
modelValue={wordForm.sentences}
onUpdate:modelValue={e => wordForm.sentences = e}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{minRows: 6, maxRows: 10}}
type="textarea"/>
autosize={{minRows: 6, maxRows: 10}}/>
</ElFormItem>
<ElFormItem label="短语">
<ElInput
<Textarea
modelValue={wordForm.phrases}
onUpdate:modelValue={e => wordForm.phrases = e}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{minRows: 6, maxRows: 10}}
type="textarea"/>
autosize={{minRows: 6, maxRows: 10}}/>
</ElFormItem>
<ElFormItem label="同义词">
<ElInput
<Textarea
modelValue={wordForm.synos}
onUpdate:modelValue={e => wordForm.synos = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 20}}
type="textarea"/>
autosize={{minRows: 6, maxRows: 20}}/>
</ElFormItem>
<ElFormItem label="同根词">
<ElInput
<Textarea
modelValue={wordForm.relWords}
onUpdate:modelValue={e => wordForm.relWords = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 20}}
type="textarea"/>
autosize={{minRows: 6, maxRows: 20}}/>
</ElFormItem>
<ElFormItem label="词源">
<ElInput
<Textarea
modelValue={wordForm.etymology}
onUpdate:modelValue={e => wordForm.etymology = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 10}}
type="textarea"/>
autosize={{minRows: 6, maxRows: 10}}/>
</ElFormItem>
</ElForm>
<div class="center">