版本发布

This commit is contained in:
icssoa
2025-07-21 16:47:04 +08:00
parent 1abed7a2e1
commit 6d8193880a
307 changed files with 41718 additions and 0 deletions

126
components/locale-set.uvue Normal file
View File

@@ -0,0 +1,126 @@
<template>
<cl-popup
v-model="visible"
direction="center"
:title="t('切换语言')"
:pt="{
className: '!rounded-3xl'
}"
>
<view class="locale-set w-[500rpx] p-4 pt-0">
<view
v-for="item in list"
:key="item.value"
class="p-2 px-3 my-1 rounded-xl flex flex-row items-center border border-solid border-surface-200"
:class="{
'!border-surface-600': isDark,
'!bg-primary-500 !border-primary-500': active == item.value
}"
@tap="change(item.value)"
>
<cl-text
:pt="{
className: parseClass([
'flex-1',
[isDark || active == item.value, '!text-white', '!text-surface-700']
])
}"
>{{ item.label }}</cl-text
>
<cl-icon
name="checkbox-circle-line"
color="white"
v-if="active == item.value"
></cl-icon>
</view>
<view class="flex flex-row mt-4">
<cl-button
size="large"
text
border
type="light"
:pt="{
className: 'flex-1 !rounded-full h-[80rpx]'
}"
@tap="close"
>{{ t("取消") }}</cl-button
>
<cl-button
size="large"
:pt="{
className: 'flex-1 !rounded-full h-[80rpx]'
}"
@tap="confirm"
>{{ t("确定") }}</cl-button
>
</view>
</view>
</cl-popup>
</template>
<script setup lang="ts">
import { isDark, parseClass } from "@/cool";
import { locale, t, setLocale } from "@/locale";
import { ref } from "vue";
type Item = {
label: string;
value: string;
};
// 语言列表
const list = [
{
label: "简体中文",
value: "zh-cn"
},
{
label: "English",
value: "en"
},
{
label: "Español",
value: "es"
}
] as Item[];
// 当前语言
const active = ref(locale.value);
// 是否可见
const visible = ref(false);
// 打开
function open() {
visible.value = true;
active.value = locale.value;
if (["zh-Hans", "zh"].some((e) => e == locale.value)) {
active.value = "zh-cn";
}
}
// 关闭
function close() {
visible.value = false;
}
// 切换语言
function change(value: string) {
active.value = value;
}
// 确定
function confirm() {
setLocale(active.value);
close();
}
defineExpose({
visible,
open,
close
});
</script>

220
components/sms-btn.uvue Normal file
View File

@@ -0,0 +1,220 @@
<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('获取短信验证码')"
>
<view class="p-3 pt-2 pb-4 w-[460rpx]" 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>
<cl-image
:src="captcha.img"
:height="70"
:width="200"
:pt="{
className: '!rounded-lg',
error: {
className: parseClass([[isDark, '!bg-surface-800', '!bg-surface-200']]),
name: 'refresh-line'
}
}"
@tap="getCaptcha"
></cl-image>
</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, parseClass, service, 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 service.user.login
.captcha({ color: isDark.value ? "#ffffff" : "#2c3142", phone: props.phone })
.then((res) => {
const data = parse<Res>(res)!;
captchaId.value = data.captchaId;
captcha.img = data.data;
})
.catch((err) => {
ui.showToast({
message: (err as Response).message!
});
});
captcha.loading = false;
}
// 发送短信
async function send() {
if (code.value != "") {
captcha.sending = true;
await service.user.login
.smsCode({
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>

77
components/tabbar.uvue Normal file
View File

@@ -0,0 +1,77 @@
<template>
<cl-footer
:pt="{
content: {
className: '!p-0 h-[60px]'
}
}"
>
<view class="tabbar" :class="{ 'is-dark': isDark }">
<view
class="tabbar-item"
v-for="item in list"
:key="item.pagePath"
@tap="router.to(item.pagePath)"
>
<cl-image
:src="router.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',
[
router.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";
type Item = {
icon: string;
icon2: string;
pagePath: string;
text: string | null;
};
// 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
uni.hideTabBar();
</script>
<style lang="scss" scoped>
.tabbar {
@apply flex flex-row items-center flex-1;
.tabbar-item {
@apply flex flex-col items-center justify-center flex-1;
}
}
</style>