feat:添加input组件
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
--article-panel-margin-left: calc(50% + var(--article-width) / 2 + 1rem);
|
||||
--anim-time: 0.3s;
|
||||
|
||||
--color-input-color: black;
|
||||
--color-input-bg: white;
|
||||
--color-input-border: #bfbfbf;
|
||||
--color-input-icon: #d3d4d7;
|
||||
@@ -98,6 +99,7 @@ html.dark {
|
||||
|
||||
--btn-info: transparent;
|
||||
|
||||
--color-input-color:white;
|
||||
--color-input-bg: rgba(14, 18, 23, 1);
|
||||
--color-input-icon: #383737;
|
||||
|
||||
|
||||
@@ -14,10 +14,11 @@ 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, ElRadio, ElRadioGroup, ElSlider} from 'element-plus'
|
||||
import {ElInputNumber, ElRadio, ElRadioGroup} 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";
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleDisabledDialogEscKey: [val: boolean]
|
||||
@@ -197,14 +198,14 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<ElSlider v-model="settingStore.wordSoundVolume"/>
|
||||
<Slider v-model="settingStore.wordSoundVolume"/>
|
||||
<span>{{ settingStore.wordSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">倍速</label>
|
||||
<div class="wrapper">
|
||||
<ElSlider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3"/>
|
||||
<Slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3"/>
|
||||
<span>{{ settingStore.wordSoundSpeed }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,7 +246,7 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<ElSlider v-model="settingStore.keyboardSoundVolume"/>
|
||||
<Slider v-model="settingStore.keyboardSoundVolume"/>
|
||||
<span>{{ settingStore.keyboardSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -263,7 +264,7 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<ElSlider v-model="settingStore.effectSoundVolume"/>
|
||||
<Slider v-model="settingStore.effectSoundVolume"/>
|
||||
<span>{{ settingStore.effectSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -337,7 +338,7 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="sub-title">外语字体</label>
|
||||
<div class="wrapper">
|
||||
<ElSlider
|
||||
<Slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordForeignFontSize"/>
|
||||
@@ -347,7 +348,7 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="sub-title">中文字体</label>
|
||||
<div class="wrapper">
|
||||
<ElSlider
|
||||
<Slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordTranslateFontSize"/>
|
||||
|
||||
@@ -53,7 +53,7 @@ const searchList = computed<any[]>(() => {
|
||||
<div class="flex items-center relative gap-2">
|
||||
<BackIcon class="z-2" @Click='router.back()'/>
|
||||
<div class="flex flex-1 gap-4" v-if="showSearchInput">
|
||||
<Input placeholder="请输入书籍名称/缩写/类别" v-model="searchKey" class="flex-1" autofocus/>
|
||||
<Input prefix-icon placeholder="请输入书籍名称/缩写/类别" v-model="searchKey" class="flex-1" autofocus/>
|
||||
<BaseButton @click="showSearchInput = false, searchKey = ''">取消</BaseButton>
|
||||
</div>
|
||||
<div class="py-1 flex flex-1 justify-end" v-else>
|
||||
|
||||
@@ -11,6 +11,8 @@ import {useBaseStore} from "@/stores/base.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {getDefaultDict} from "@/types/func.ts";
|
||||
import {Option, Select} from "@/pages/pc/components/base/select";
|
||||
import Input from "@/pages/pc/components/Input.vue";
|
||||
import BaseInput from "@/pages/pc/components/base/BaseInput.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
isAdd: boolean,
|
||||
@@ -99,10 +101,10 @@ onMounted(() => {
|
||||
:model="dictForm"
|
||||
label-width="8rem">
|
||||
<ElFormItem label="名称" prop="name">
|
||||
<ElInput v-model="dictForm.name"/>
|
||||
<BaseInput v-model="dictForm.name"/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述">
|
||||
<ElInput v-model="dictForm.description" type="textarea"/>
|
||||
<BaseInput v-model="dictForm.description" textarea/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="原文语言">
|
||||
<Select v-model="dictForm.language" placeholder="请选择选项">
|
||||
|
||||
@@ -134,6 +134,7 @@ defineRender(
|
||||
class="flex gap-4"
|
||||
>
|
||||
<Input
|
||||
prefixIcon
|
||||
modelValue={searchKey}
|
||||
onUpdate:modelValue=
|
||||
{debounce(e => searchKey = e)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import BaseButton from "@/components/BaseButton.vue";
|
||||
import {ElInput} from "element-plus";
|
||||
|
||||
import {watchEffect} from "vue";
|
||||
import BaseInput from "@/pages/pc/components/base/BaseInput.vue";
|
||||
|
||||
interface IProps {
|
||||
value: string,
|
||||
@@ -38,9 +39,10 @@ function toggle() {
|
||||
<div
|
||||
v-if="edit"
|
||||
class="edit-text">
|
||||
<ElInput
|
||||
<BaseInput
|
||||
v-model="editVal"
|
||||
ref="inputRef"
|
||||
textarea
|
||||
autosize
|
||||
autofocus
|
||||
type="textarea"
|
||||
|
||||
@@ -8,6 +8,7 @@ defineProps<{
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
autofocus?: boolean
|
||||
prefixIcon?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
@@ -37,6 +38,7 @@ const vFocus = {
|
||||
ref="inputEl"
|
||||
>
|
||||
<Icon icon="fluent:search-24-regular"
|
||||
v-if="prefixIcon"
|
||||
width="20"/>
|
||||
<input type="text"
|
||||
:value="modelValue"
|
||||
@@ -61,7 +63,6 @@ const vFocus = {
|
||||
transition: all .3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all .3s;
|
||||
background: var(--color-input-bg);
|
||||
|
||||
:deep(svg) {
|
||||
|
||||
278
src/pages/pc/components/base/BaseInput.vue
Normal file
278
src/pages/pc/components/base/BaseInput.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, watch, defineProps, defineEmits, useAttrs, nextTick, PropType} from 'vue';
|
||||
|
||||
interface Autosize {
|
||||
minRows?: number;
|
||||
maxRows?: number;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String, Number],
|
||||
placeholder: String,
|
||||
disabled: Boolean,
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
clearable: {
|
||||
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;
|
||||
validate(val);
|
||||
});
|
||||
|
||||
const validate = (val: string | number | null | undefined) => {
|
||||
let err = '';
|
||||
const strVal = val == null ? '' : String(val);
|
||||
if (props.required && !strVal.trim()) {
|
||||
err = '不能为空';
|
||||
} else if (props.maxLength && strVal.length > props.maxLength) {
|
||||
err = `长度不能超过 ${props.maxLength} 个字符`;
|
||||
}
|
||||
errorMsg.value = err;
|
||||
emit('validation', err === '', err);
|
||||
return err === '';
|
||||
};
|
||||
|
||||
const onInput = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement | HTMLTextAreaElement;
|
||||
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) => {
|
||||
emit('change', e);
|
||||
};
|
||||
|
||||
const onFocus = (e: FocusEvent) => {
|
||||
emit('focus', e);
|
||||
};
|
||||
|
||||
const onBlur = (e: FocusEvent) => {
|
||||
validate(inputValue.value);
|
||||
emit('blur', e);
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<div v-if="errorMsg" class="custom-input__error">{{ errorMsg }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-input {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.has-error {
|
||||
.custom-input__inner,
|
||||
.custom-input__textarea {
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
|
||||
.custom-input__error {
|
||||
color: #f56c6c;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__inner,
|
||||
&__textarea {
|
||||
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);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 0 3px #409eff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&__textarea {
|
||||
min-height: 5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__clear {
|
||||
position: absolute;
|
||||
right: 0.4rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
&__error {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-input__textarea {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
</style>
|
||||
236
src/pages/pc/components/base/Slider.vue
Normal file
236
src/pages/pc/components/base/Slider.vue
Normal file
@@ -0,0 +1,236 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, watch, defineProps, defineEmits, onMounted, nextTick} from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
disabled?: boolean;
|
||||
showText?: boolean;
|
||||
showValue?: boolean; // 是否显示当前值
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const min = props.min ?? 0;
|
||||
const max = props.max ?? 100;
|
||||
const step = props.step ?? 1;
|
||||
|
||||
const sliderRef = ref<HTMLElement | null>(null);
|
||||
const isDragging = ref(false);
|
||||
const sliderLeft = ref(0);
|
||||
const sliderWidth = ref(0);
|
||||
|
||||
const currentValue = ref(props.modelValue);
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
currentValue.value = val;
|
||||
});
|
||||
|
||||
const valueToPercent = (value: number) => ((value - min) / (max - min)) * 100;
|
||||
|
||||
// 计算一个数字的小数位数
|
||||
function countDecimals(value: number) {
|
||||
if (Math.floor(value) === value) return 0;
|
||||
const str = value.toString();
|
||||
if (str.indexOf('e-') >= 0) {
|
||||
// 科学计数法处理
|
||||
const [, trail] = str.split('e-');
|
||||
return parseInt(trail, 10);
|
||||
}
|
||||
return str.split('.')[1]?.length || 0;
|
||||
}
|
||||
|
||||
// 对数值按步长对齐,并控制精度,避免浮点误差
|
||||
function alignToStep(value: number, step: number) {
|
||||
const decimals = countDecimals(step);
|
||||
return Number((Math.round(value / step) * step).toFixed(decimals));
|
||||
}
|
||||
|
||||
const percentToValue = (percent: number) => {
|
||||
let val = min + ((max - min) * percent) / 100;
|
||||
val = alignToStep(val, step);
|
||||
|
||||
if (val < min) val = min;
|
||||
if (val > max) val = max;
|
||||
|
||||
return val;
|
||||
};
|
||||
|
||||
|
||||
const updateSliderRect = () => {
|
||||
if (!sliderRef.value) return;
|
||||
const rect = sliderRef.value.getBoundingClientRect();
|
||||
sliderLeft.value = rect.left;
|
||||
sliderWidth.value = rect.width;
|
||||
};
|
||||
|
||||
const setValueFromPosition = (pageX: number) => {
|
||||
let percent = ((pageX - sliderLeft.value) / sliderWidth.value) * 100;
|
||||
if (percent < 0) percent = 0;
|
||||
if (percent > 100) percent = 100;
|
||||
currentValue.value = percentToValue(percent);
|
||||
emit('update:modelValue', currentValue.value);
|
||||
};
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (props.disabled) return;
|
||||
e.preventDefault();
|
||||
updateSliderRect();
|
||||
isDragging.value = true;
|
||||
setValueFromPosition(e.pageX);
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
if (props.disabled) return;
|
||||
updateSliderRect();
|
||||
isDragging.value = true;
|
||||
setValueFromPosition(e.touches[0].pageX);
|
||||
window.addEventListener('touchmove', onTouchMove);
|
||||
window.addEventListener('touchend', onTouchEnd);
|
||||
};
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging.value) return;
|
||||
e.preventDefault();
|
||||
setValueFromPosition(e.pageX);
|
||||
};
|
||||
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
if (!isDragging.value) return;
|
||||
setValueFromPosition(e.touches[0].pageX);
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
if (!isDragging.value) return;
|
||||
isDragging.value = false;
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (!isDragging.value) return;
|
||||
isDragging.value = false;
|
||||
window.removeEventListener('touchmove', onTouchMove);
|
||||
window.removeEventListener('touchend', onTouchEnd);
|
||||
};
|
||||
|
||||
const onClickTrack = (e: MouseEvent) => {
|
||||
if (props.disabled) return;
|
||||
updateSliderRect();
|
||||
setValueFromPosition(e.pageX);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
updateSliderRect();
|
||||
window.addEventListener('resize', updateSliderRect);
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div
|
||||
ref="sliderRef"
|
||||
class="custom-slider"
|
||||
:class="{ 'is-disabled': disabled }"
|
||||
@mousedown="onClickTrack"
|
||||
@touchstart.prevent="onClickTrack"
|
||||
>
|
||||
<div class="custom-slider__track"></div>
|
||||
<div
|
||||
class="custom-slider__fill"
|
||||
:style="{ width: valueToPercent(currentValue) + '%' }"
|
||||
></div>
|
||||
<div
|
||||
class="custom-slider__thumb"
|
||||
:style="{ left: valueToPercent(currentValue) + '%' }"
|
||||
@mousedown.stop.prevent="onMouseDown"
|
||||
@touchstart.stop.prevent="onTouchStart"
|
||||
tabindex="0"
|
||||
role="slider"
|
||||
:aria-valuemin="min"
|
||||
:aria-valuemax="max"
|
||||
:aria-valuenow="currentValue"
|
||||
:aria-disabled="disabled"
|
||||
></div>
|
||||
<div v-if="showValue" class="custom-slider__value">{{ currentValue }}</div>
|
||||
</div>
|
||||
<div class="text flex justify-between text-sm color-gray" v-if="showText">
|
||||
<span>{{ min }}</span>
|
||||
<span>{{ max }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-slider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
cursor: pointer;
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&__track {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 6px;
|
||||
background-color: #ddd;
|
||||
border-radius: 2px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&__fill {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
height: 6px;
|
||||
background-color: #409eff;
|
||||
border-radius: 2px 0 0 2px;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__thumb {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #fff;
|
||||
border: 2px solid #409eff;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor: grab;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
&__thumb:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 5px #409eff;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&__value {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 4px);
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -48,7 +48,7 @@ defineExpose({scrollToBottom, scrollToItem})
|
||||
<template>
|
||||
<div class="list">
|
||||
<div class="search">
|
||||
<Input v-model="searchKey"/>
|
||||
<Input prefix-icon v-model="searchKey"/>
|
||||
</div>
|
||||
<BaseList
|
||||
ref="listRef"
|
||||
|
||||
@@ -100,7 +100,7 @@ defineExpose({scrollBottom})
|
||||
ref="el"
|
||||
>
|
||||
<div class="search">
|
||||
<Input v-model="searchKey"/>
|
||||
<Input prefix-icon v-model="searchKey"/>
|
||||
</div>
|
||||
<transition-group name="drag" class="list" tag="div">
|
||||
<div class="item"
|
||||
|
||||
@@ -19,6 +19,7 @@ import {useRoute, useRouter} from "vue-router";
|
||||
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";
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const base = useBaseStore()
|
||||
@@ -268,19 +269,19 @@ defineRender(() => {
|
||||
model={wordForm}
|
||||
label-width="7rem">
|
||||
<ElFormItem label="单词" prop="word">
|
||||
<ElInput
|
||||
<BaseInput
|
||||
modelValue={wordForm.word}
|
||||
onUpdate:modelValue={e => wordForm.word = e}
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="英音音标">
|
||||
<ElInput
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic0}
|
||||
onUpdate:modelValue={e => wordForm.phonetic0 = e}
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="美音音标">
|
||||
<ElInput
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic1}
|
||||
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
|
||||
</ElFormItem>
|
||||
|
||||
@@ -80,7 +80,7 @@ const searchList = computed<any[]>(() => {
|
||||
<div class="flex items-center relative gap-2">
|
||||
<BackIcon class="z-2" @Click='router.back()'/>
|
||||
<div class="flex flex-1 gap-4" v-if="showSearchInput">
|
||||
<Input placeholder="请输入词典名称/缩写/类别" v-model="searchKey" class="flex-1" autofocus/>
|
||||
<Input prefix-icon placeholder="请输入词典名称/缩写/类别" v-model="searchKey" class="flex-1" autofocus/>
|
||||
<BaseButton @click="showSearchInput = false, searchKey = ''">取消</BaseButton>
|
||||
</div>
|
||||
<div class="py-1 flex flex-1 justify-end" v-else>
|
||||
|
||||
@@ -12,11 +12,11 @@ import {getCurrentStudyWord} from "@/hooks/dict.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import Book from "@/pages/pc/components/Book.vue";
|
||||
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
|
||||
import {ElSlider} from 'element-plus';
|
||||
import Progress from '@/pages/pc/components/base/Progress.vue';
|
||||
import Toast from '@/pages/pc/components/base/toast/Toast.ts';
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {getDefaultDict} from "@/types/func.ts";
|
||||
import Slider from "@/pages/pc/components/base/Slider.vue";
|
||||
|
||||
const store = useBaseStore()
|
||||
const router = useRouter()
|
||||
@@ -224,9 +224,13 @@ const progressTextRight = $computed(() => {
|
||||
<div class="center text-sm" :style="{ opacity: tempPerDayStudyNumber === 20 ? 1 : 0 }">
|
||||
推荐
|
||||
</div>
|
||||
<ElSlider :min="10" :step="10" show-stops :marks="{ 10: '10', 200: '200' }" size="small" class="my-6"
|
||||
:max="200" v-model="tempPerDayStudyNumber"/>
|
||||
<div class="flex gap-2 mb-2 mt-10 items-center">
|
||||
<Slider :min="10"
|
||||
:step="10"
|
||||
show-stops
|
||||
class="mt-3"
|
||||
show-text
|
||||
:max="200" v-model="tempPerDayStudyNumber"/>
|
||||
<div class="flex gap-2 mb-2 mt-2 items-center">
|
||||
<div>预计</div>
|
||||
<span class="text-2xl" style="color:rgb(176,116,211)">{{
|
||||
_getAccomplishDays(store.sdict.words.length, tempPerDayStudyNumber)
|
||||
|
||||
Reference in New Issue
Block a user