小程序初始提交

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,84 @@
<template>
<cl-select
ref="selectRef"
v-model="active"
:options="options"
:show-trigger="false"
:title="t('切换语言')"
:cancel-text="t('取消')"
:confirm-text="t('确定')"
></cl-select>
</template>
<script setup lang="ts">
import { locale, setLocale, t } from "@/locale";
import { useUi, type ClSelectOption } from "@/uni_modules/cool-ui";
import { ref } from "vue";
const ui = useUi();
// 语言列表
const options = [
{
label: "简体中文",
value: "zh-cn"
},
{
label: "繁体中文",
value: "zh-tw"
},
{
label: "English",
value: "en"
},
{
label: "Español",
value: "es"
},
{
label: "日本語",
value: "ja"
},
{
label: "한국어",
value: "ko"
},
{
label: "Français",
value: "fr"
}
] as ClSelectOption[];
const selectRef = ref<ClSelectComponentPublicInstance | null>(null);
// 当前语言
const active = ref(locale.value);
// 打开
function open() {
active.value = locale.value;
if (["zh-Hans", "zh"].some((e) => e == locale.value)) {
active.value = "zh-cn";
}
selectRef.value!.open((value) => {
ui.showLoading(t("切换中"));
setTimeout(() => {
setLocale(value as string);
ui.hideLoading();
}, 500);
});
}
// 关闭
function close() {
selectRef.value!.close();
}
defineExpose({
open,
close
});
</script>

View File

@@ -0,0 +1,94 @@
<template>
<cl-select
ref="selectRef"
v-model="size"
:title="t('全局字号')"
:options="list"
:show-trigger="false"
@changing="onChanging"
>
<template #prepend>
<view class="px-3 absolute top-0 left-0 z-10">
<cl-text
:style="{
fontSize: 28 * size + 'rpx'
}"
>{{ t("这是一段示例文字,用于预览不同字号的效果。") }}</cl-text
>
</view>
</template>
</cl-select>
</template>
<script setup lang="ts">
import { t } from "@/locale";
import { type ClSelectOption } from "@/uni_modules/cool-ui";
import { config } from "@/uni_modules/cool-ui/config";
import { ref } from "vue";
defineOptions({
name: "size-set"
});
const selectRef = ref<ClSelectComponentPublicInstance | null>(null);
// 语言列表
const list = [
{
label: "0.9",
value: 0.9
},
{
label: t("默认 1.0"),
value: 1
},
{
label: "1.1",
value: 1.1
},
{
label: "1.2",
value: 1.2
},
{
label: "1.3",
value: 1.3
},
{
label: "1.4",
value: 1.4
}
] as ClSelectOption[];
// 当前语言
const size = ref(1);
// 是否可见
const visible = ref(false);
// 打开
function open() {
visible.value = true;
size.value = config.fontSize ?? 1;
selectRef.value!.open((value) => {
config.fontSize = value == 1 ? null : (value as number);
});
}
// 关闭
function close() {
visible.value = false;
}
// 切换
function onChanging(value: number) {
size.value = value;
}
defineExpose({
visible,
open,
close
});
</script>

View File

@@ -0,0 +1,234 @@
<template>
<slot :disabled="isDisabled" :countdown="countdown" :btnText="btnText">
<cl-button text :disabled="isDisabled" @tap="open">
{{ btnText }}
</cl-button>
</slot>
<cl-popup
v-model="captcha.visible"
ref="popupRef"
direction="center"
:title="t('获取短信验证码')"
:size="500"
>
<view class="p-3 pt-2 pb-4 w-full" v-if="captcha.visible">
<view class="flex flex-row items-center">
<cl-input
v-model="code"
:placeholder="t('验证码')"
:maxlength="4"
autofocus
:clearable="false"
:pt="{
className: 'flex-1 mr-2 !h-[70rpx]'
}"
@confirm="send"
></cl-input>
<view
class="dark:!bg-surface-800 bg-surface-100 rounded-lg h-[70rpx] w-[200rpx] flex flex-row justify-center items-center"
@tap="getCaptcha"
>
<cl-loading v-if="captcha.loading" :size="28"></cl-loading>
<cl-svg
v-else
class="h-full w-full pointer-events-none"
color="none"
:src="captcha.img"
></cl-svg>
</view>
</view>
<cl-button
type="primary"
:disabled="code == ''"
:loading="captcha.sending"
:pt="{
className: '!h-[70rpx] mt-3'
}"
@tap="send"
>
{{ t("发送短信") }}
</cl-button>
</view>
</cl-popup>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from "vue";
import { useUi } from "@/uni_modules/cool-ui";
import { $t, t } from "@/locale";
import { isDark, parse, request, type Response } from "@/cool";
const props = defineProps({
phone: String
});
const emit = defineEmits(["success"]);
const popupRef = ref<ClPopupComponentPublicInstance | null>(null);
const ui = useUi();
type Captcha = {
visible: boolean;
loading: boolean;
sending: boolean;
img: string;
};
// 验证码
const captcha = reactive<Captcha>({
visible: false,
loading: false,
sending: false,
img: ""
});
// 倒计时
const countdown = ref(0);
// 是否禁用
const isDisabled = computed(() => countdown.value > 0 || props.phone == "");
// 按钮文案
const btnText = computed(() =>
countdown.value > 0 ? $t("{n}s后重新获取", { n: countdown.value }) : t("获取验证码")
);
const code = ref("");
const captchaId = ref("");
// 清空
function clear() {
code.value = "";
captchaId.value = "";
}
// 关闭
function close() {
captcha.visible = false;
captcha.img = "";
clear();
}
// 开始倒计时
function startCountdown() {
countdown.value = 60;
let timer: number = 0;
function fn() {
countdown.value--;
if (countdown.value < 1) {
clearInterval(timer);
}
}
// @ts-ignore
timer = setInterval(() => {
fn();
}, 1000);
fn();
}
// 获取图片验证码
async function getCaptcha() {
clear();
captcha.loading = true;
type Res = {
captchaId: string;
data: string;
};
await request({
url: "/app/user/login/captcha",
data: {
color: isDark.value ? "#ffffff" : "#2c3142",
phone: props.phone,
width: 200,
height: 70
}
})
.then((res) => {
if (res != null) {
const data = parse<Res>(res)!;
captchaId.value = data.captchaId;
captcha.img = data.data;
}
})
.catch((err) => {
ui.showToast({
message: (err as Response).message!
});
});
setTimeout(() => {
captcha.loading = false;
}, 200);
}
// 发送短信
async function send() {
if (code.value != "") {
captcha.sending = true;
await request({
url: "/app/user/login/smsCode",
method: "POST",
data: {
phone: props.phone,
code: code.value,
captchaId: captchaId.value
}
})
.then(() => {
ui.showToast({
message: t("短信已发送,请查收")
});
startCountdown();
close();
emit("success");
})
.catch((err) => {
ui.showToast({
message: (err as Response).message!
});
getCaptcha();
});
captcha.sending = false;
} else {
ui.showToast({
message: t("请填写验证码")
});
}
}
// 打开
function open() {
if (props.phone != "") {
if (/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(props.phone!)) {
captcha.visible = true;
getCaptcha();
} else {
ui.showToast({
message: t("请填写正确的手机号格式")
});
}
}
}
defineExpose({
open,
send,
getCaptcha,
startCountdown
});
</script>

View File

@@ -0,0 +1,83 @@
<template>
<cl-footer
:pt="{
content: {
className: '!p-0 h-[60px]'
}
}"
>
<view class="custom-tabbar" :class="{ 'is-dark': isDark }">
<view
class="custom-tabbar-item"
v-for="item in list"
:key="item.pagePath"
@tap="router.to(item.pagePath)"
>
<cl-image
:src="path == item.pagePath ? item.icon2 : item.icon"
:height="56"
:width="56"
></cl-image>
<cl-text
v-if="item.text != null"
:pt="{
className: parseClass([
'text-xs mt-1',
[path == item.pagePath, 'text-primary-500', 'text-surface-400']
])
}"
>{{ t(item.text!) }}</cl-text
>
</view>
</view>
</cl-footer>
</template>
<script setup lang="ts">
import { ctx, isDark, parseClass, router } from "@/cool";
import { t } from "@/locale";
import { computed } from "vue";
defineOptions({
name: "custom-tabbar"
});
type Item = {
icon: string;
icon2: string;
pagePath: string;
text: string | null;
};
const path = computed(() => router.path());
// tabbar 列表
const list = computed<Item[]>(() => {
return (ctx.tabBar.list ?? []).map((e) => {
return {
icon: e.iconPath!,
icon2: e.selectedIconPath!,
pagePath: e.pagePath,
text: t(e.text?.replaceAll("%", "")!)
} as Item;
});
});
// 隐藏原生 tabBar
// #ifndef MP
if (ctx.tabBar.list != null) {
uni.hideTabBar();
}
// #endif
</script>
<style lang="scss" scoped>
.custom-tabbar {
@apply flex flex-row items-center flex-1;
&-item {
@apply flex flex-col items-center justify-center flex-1;
}
}
</style>