小程序初始提交

This commit is contained in:
jdc
2025-11-13 10:36:23 +08:00
parent f26b4f9a2f
commit 5db3b180eb
447 changed files with 83351 additions and 0 deletions

View File

@@ -0,0 +1,450 @@
<template>
<cl-popup
:title="title"
:swipe-close-threshold="100"
:pt="{
inner: {
className: parseClass([
[isDark, '!bg-surface-700', '!bg-surface-100'],
pt.popup?.className
])
},
mask: {
className: '!bg-transparent'
}
}"
v-model="visible"
>
<view class="cl-keyboard-password" :class="[pt.className]">
<slot name="value" :value="value">
<view
v-if="showValue"
class="cl-keyboard-password__value"
:class="[pt.value?.className]"
>
<cl-text
v-if="value != ''"
:pt="{
className: 'text-2xl'
}"
>{{ valueText }}</cl-text
>
<cl-text
v-else
:pt="{
className: 'text-md text-surface-400'
}"
>{{ placeholder }}</cl-text
>
</view>
</slot>
<view class="cl-keyboard-password__list">
<view
class="cl-keyboard-password__rows"
v-for="(row, rowIndex) in list"
:key="rowIndex"
:class="[`is-mode-${mode}`]"
>
<view
v-for="(item, index) in row"
:key="item"
class="cl-keyboard-password__item"
:class="[
`is-keycode-${item}`,
{
'is-empty': item == '',
'is-dark': isDark
},
pt.item?.className
]"
:style="{
marginRight: index == row.length - 1 ? '0' : '10rpx'
}"
hover-class="opacity-50"
:hover-stay-time="250"
@touchstart.stop="onCommand(item)"
>
<slot name="item" :item="item">
<cl-icon
v-if="item == 'delete'"
name="delete-back-2-line"
:size="36"
></cl-icon>
<cl-text
v-else-if="item == 'confirm'"
color="white"
:pt="{
className: 'text-lg'
}"
>{{ confirmText }}</cl-text
>
<cl-text
v-else-if="item == 'letter'"
:pt="{
className: 'text-lg'
}"
>ABC</cl-text
>
<cl-text
v-else-if="item == 'number'"
:pt="{
className: 'text-lg'
}"
>123</cl-text
>
<template v-else-if="item == 'caps'">
<cl-icon name="upload-line" :size="36"></cl-icon>
<cl-badge
dot
position
type="info"
:pt="{
className: '!right-1 !top-1'
}"
v-if="mode == 'letterUpper'"
></cl-badge>
</template>
<cl-text
v-else
:pt="{
className: 'text-lg'
}"
>{{ item }}</cl-text
>
</slot>
</view>
</view>
</view>
</view>
</cl-popup>
</template>
<script setup lang="ts">
import { useUi } from "../../hooks";
import type { PassThroughProps } from "../../types";
import type { ClPopupProps } from "../cl-popup/props";
import { ref, computed, watch } from "vue";
import { $t, t } from "@/locale";
import { isDark, parseClass, parsePt } from "@/cool";
import { vibrate } from "@/uni_modules/cool-vibrate";
defineOptions({
name: "cl-keyboard-password"
});
defineSlots<{
value(props: { value: string }): any;
item(props: { item: string }): any;
}>();
const props = defineProps({
// 透传样式配置
pt: {
type: Object,
default: () => ({})
},
// v-model绑定的值
modelValue: {
type: String,
default: ""
},
// 弹窗标题
title: {
type: String,
default: () => t("密码键盘")
},
// 输入框占位符
placeholder: {
type: String,
default: () => t("安全键盘,请放心输入")
},
// 最小输入长度
minlength: {
type: Number,
default: 6
},
// 最大输入长度
maxlength: {
type: Number,
default: 20
},
// 确认按钮文本
confirmText: {
type: String,
default: () => t("确定")
},
// 是否显示输入值
showValue: {
type: Boolean,
default: true
},
// 是否输入即绑定
inputImmediate: {
type: Boolean,
default: false
},
// 是否加密
encrypt: {
type: Boolean,
default: false
}
});
// 定义事件发射器支持v-model和change事件
const emit = defineEmits(["update:modelValue", "change"]);
// 样式穿透类型
type PassThrough = {
className?: string;
item?: PassThroughProps;
value?: PassThroughProps;
popup?: ClPopupProps;
};
// 样式穿透计算
const pt = computed(() => parsePt<PassThrough>(props.pt));
// 获取UI相关的工具方法
const ui = useUi();
// 控制弹窗显示/隐藏
const visible = ref(false);
// 输入框当前值,双向绑定
const value = ref(props.modelValue);
// 键盘模式letter: 字母区number: 数字区
const mode = ref<"letter" | "letterUpper" | "number">("letter");
// 输入框显示值
const valueText = computed(() => {
if (props.encrypt) {
return "*".repeat(value.value.length);
}
return value.value;
});
// 数字键盘的按键列表包含数字、删除、00和小数点
const list = computed(() => {
// 字母键盘的字母区
const letter: string[][] = [
["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
["a", "s", "d", "f", "g", "h", "j", "k", "l", "m"],
["caps", "z", "x", "c", "v", "b", "n", "m", "delete"],
["number", "space", "confirm"]
];
// 大写字母键盘的字母区
const letterUpper: string[][] = [
["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
["A", "S", "D", "F", "G", "H", "J", "K", "L", "M"],
["caps", "Z", "X", "C", "V", "B", "N", "M", "delete"],
["number", "space", "confirm"]
];
// 数字键盘的数字区
const number: string[][] = [
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
["#", "/", ":", ";", "(", ")", "^", "*", "+"],
["-", "=", "|", "~", "$", "&", ".", ",", "delete"],
["letter", "%", "?", "!", "{", "}", "confirm"]
];
switch (mode.value) {
case "letter":
return letter;
case "letterUpper":
return letterUpper;
case "number":
return number;
default:
return letter;
}
});
// 打开键盘弹窗
function open() {
visible.value = true;
}
// 关闭键盘弹窗
function close() {
visible.value = false;
}
// 处理键盘按键点击事件
function onCommand(key: string) {
// 震动
vibrate(1);
// 大写字母键盘
if (key == "caps") {
mode.value = mode.value == "letter" ? "letterUpper" : "letter";
return;
}
// 字母键盘
if (key == "letter") {
mode.value = "letter";
return;
}
// 数字键盘
if (key == "number") {
mode.value = "number";
return;
}
// 确认按钮逻辑
if (key == "confirm") {
if (value.value == "") {
ui.showToast({
message: t("请输入内容")
});
return;
}
// 校验密码长度
if (value.value.length < props.minlength || value.value.length > props.maxlength) {
ui.showToast({
message: $t("请输入{minlength}到{maxlength}位密码", {
minlength: props.minlength,
maxlength: props.maxlength
})
});
return;
}
// 触发v-model和change事件
emit("update:modelValue", value.value);
emit("change", value.value);
// 关闭弹窗
close();
return;
}
// 删除键,去掉最后一位
if (key == "delete") {
value.value = value.value.slice(0, -1);
return;
}
// 超过最大输入长度,提示并返回
if (value.value.length >= props.maxlength) {
ui.showToast({
message: $t("最多输入{maxlength}位", {
maxlength: props.maxlength
})
});
return;
}
// 其他按键直接拼接到value
value.value += key;
}
watch(value, (val: string) => {
// 如果输入即绑定,则立即更新绑定值
if (props.inputImmediate) {
emit("update:modelValue", val);
emit("change", val);
}
});
// 监听外部v-model的变化保持内部value同步
watch(
computed(() => props.modelValue),
(val: string) => {
value.value = val;
}
);
defineExpose({
open,
close
});
</script>
<style lang="scss" scoped>
.cl-keyboard-password {
padding: 0 20rpx 20rpx 20rpx;
&__value {
@apply flex flex-row items-center justify-center;
height: 80rpx;
margin-bottom: 20rpx;
}
&__list {
@apply relative;
}
&__rows {
@apply flex flex-row items-center;
&.is-mode-province {
@apply justify-center;
}
&.is-mode-plate {
@apply justify-between;
}
}
&__item {
@apply flex items-center justify-center rounded-lg relative bg-white;
height: 80rpx;
width: 62rpx;
margin-top: 10rpx;
flex: 1;
&.is-dark {
@apply bg-surface-800;
}
&.is-keycode-number,
&.is-keycode-letter {
width: 150rpx;
flex: none;
}
&.is-keycode-caps,
&.is-keycode-delete {
width: 80rpx;
flex: none;
}
&.is-keycode-letter,
&.is-keycode-number,
&.is-keycode-caps,
&.is-keycode-delete {
@apply bg-surface-200;
&.is-dark {
@apply bg-surface-600;
}
}
&.is-keycode-confirm {
@apply bg-primary-500;
width: 150rpx !important;
flex: none;
}
&.is-empty {
background-color: transparent !important;
}
}
}
</style>

View File

@@ -0,0 +1,23 @@
import type { PassThroughProps } from "../../types";
import type { ClPopupProps } from "../cl-popup/props";
export type ClKeyboardPasswordPassThrough = {
className?: string;
item?: PassThroughProps;
value?: PassThroughProps;
popup?: ClPopupProps;
};
export type ClKeyboardPasswordProps = {
className?: string;
pt?: ClKeyboardPasswordPassThrough;
modelValue?: string;
title?: string;
placeholder?: string;
minlength?: number;
maxlength?: number;
confirmText?: string;
showValue?: boolean;
inputImmediate?: boolean;
encrypt?: boolean;
};