This commit is contained in:
2025-12-16 20:20:53 +07:00
commit 2e651f1c89
315 changed files with 33529 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { useElementSize } from '@vueuse/core';
import BScroll from '@better-scroll/core';
import type { Options } from '@better-scroll/core';
defineOptions({ name: 'BetterScroll' });
interface Props {
/**
* BetterScroll options
*
* @link https://better-scroll.github.io/docs/zh-CN/guide/base-scroll-options.html
*/
options: Options;
}
const props = defineProps<Props>();
const bsWrapper = ref<HTMLElement>();
const bsContent = ref<HTMLElement>();
const { width: wrapWidth } = useElementSize(bsWrapper);
const { width, height } = useElementSize(bsContent);
const instance = ref<BScroll>();
const isScrollY = computed(() => Boolean(props.options.scrollY));
function initBetterScroll() {
if (!bsWrapper.value) return;
instance.value = new BScroll(bsWrapper.value, props.options);
}
// refresh BS when scroll element size changed
watch([() => wrapWidth.value, () => width.value, () => height.value], () => {
instance.value?.refresh();
});
onMounted(() => {
initBetterScroll();
});
defineExpose({ instance });
</script>
<template>
<div ref="bsWrapper" class="h-full text-left">
<div ref="bsContent" class="inline-block" :class="{ 'h-full': !isScrollY }">
<slot></slot>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import type { PopoverPlacement } from 'naive-ui';
import { twMerge } from 'tailwind-merge';
defineOptions({
name: 'ButtonIcon',
inheritAttrs: false
});
interface Props {
/** Button class */
class?: string;
/** Iconify icon name */
icon?: string;
/** Tooltip content */
tooltipContent?: string;
/** Tooltip placement */
tooltipPlacement?: PopoverPlacement;
zIndex?: number;
}
const props = withDefaults(defineProps<Props>(), {
class: '',
icon: '',
tooltipContent: '',
tooltipPlacement: 'bottom',
zIndex: 98
});
const DEFAULT_CLASS = 'h-[36px] text-icon';
</script>
<template>
<NTooltip :placement="tooltipPlacement" :z-index="zIndex" :disabled="!tooltipContent">
<template #trigger>
<NButton quaternary :class="twMerge(DEFAULT_CLASS, props.class)" v-bind="$attrs">
<div class="flex-center gap-8px">
<slot>
<SvgIcon :icon="icon" />
</slot>
</div>
</NButton>
</template>
{{ tooltipContent }}
</NTooltip>
</template>
<style scoped></style>

View File

@@ -0,0 +1,88 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { TransitionPresets, useTransition } from '@vueuse/core';
defineOptions({
name: 'CountTo'
});
interface Props {
startValue?: number;
endValue?: number;
duration?: number;
autoplay?: boolean;
decimals?: number;
prefix?: string;
suffix?: string;
separator?: string;
decimal?: string;
useEasing?: boolean;
transition?: keyof typeof TransitionPresets;
}
const props = withDefaults(defineProps<Props>(), {
startValue: 0,
endValue: 2021,
duration: 1500,
autoplay: true,
decimals: 0,
prefix: '',
suffix: '',
separator: ',',
decimal: '.',
useEasing: true,
transition: 'linear'
});
const source = ref(props.startValue);
const transition = computed(() => (props.useEasing ? TransitionPresets[props.transition] : undefined));
const outputValue = useTransition(source, {
disabled: false,
duration: props.duration,
transition: transition.value
});
const value = computed(() => formatValue(outputValue.value));
function formatValue(num: number) {
const { decimals, decimal, separator, suffix, prefix } = props;
let number = num.toFixed(decimals);
number = String(number);
const x = number.split('.');
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : '';
const rgx = /(\d+)(\d{3})/;
if (separator) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, `$1${separator}$2`);
}
}
return prefix + x1 + x2 + suffix;
}
async function start() {
await nextTick();
source.value = props.endValue;
}
watch(
[() => props.startValue, () => props.endValue],
() => {
if (props.autoplay) {
start();
}
},
{ immediate: true }
);
</script>
<template>
<span>{{ value }}</span>
</template>
<style scoped></style>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { $t } from '@/locales';
defineOptions({
name: 'LookForward'
});
</script>
<template>
<div class="size-full min-h-520px flex-col-center gap-24px overflow-hidden">
<div class="flex text-400px text-primary">
<SvgIcon local-icon="expectation" />
</div>
<slot>
<h3 class="text-28px text-primary font-500">{{ $t('common.lookForward') }}</h3>
</slot>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
defineOptions({
name: 'SoybeanAvatar'
});
</script>
<template>
<div class="size-72px overflow-hidden rd-1/2">
<img src="@/assets/imgs/soybean.jpg" class="size-full" />
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,54 @@
<script setup lang="ts">
import { computed, useAttrs } from 'vue';
import { Icon } from '@iconify/vue';
defineOptions({ name: 'SvgIcon', inheritAttrs: false });
/**
* Props
*
* - Support iconify and local svg icon
* - If icon and localIcon are passed at the same time, localIcon will be rendered first
*/
interface Props {
/** Iconify icon name */
icon?: string;
/** Local svg icon name */
localIcon?: string;
}
const props = defineProps<Props>();
const attrs = useAttrs();
const bindAttrs = computed<{ class: string; style: string }>(() => ({
class: (attrs.class as string) || '',
style: (attrs.style as string) || ''
}));
const symbolId = computed(() => {
const { VITE_ICON_LOCAL_PREFIX: prefix } = import.meta.env;
const defaultLocalIcon = 'no-icon';
const icon = props.localIcon || defaultLocalIcon;
return `#${prefix}-${icon}`;
});
/** If localIcon is passed, render localIcon first */
const renderLocalIcon = computed(() => props.localIcon || !props.icon);
</script>
<template>
<template v-if="renderLocalIcon">
<svg aria-hidden="true" width="1em" height="1em" v-bind="bindAttrs">
<use :xlink:href="symbolId" fill="currentColor" />
</svg>
</template>
<template v-else>
<Icon v-if="icon" :icon="icon" v-bind="bindAttrs" />
</template>
</template>
<style scoped></style>

View File

@@ -0,0 +1,61 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { getPaletteColorByNumber } from '@sa/color';
defineOptions({ name: 'WaveBg' });
interface Props {
/** Theme color */
themeColor: string;
}
const props = defineProps<Props>();
const lightColor = computed(() => getPaletteColorByNumber(props.themeColor, 200));
const darkColor = computed(() => getPaletteColorByNumber(props.themeColor, 500));
</script>
<template>
<div class="absolute-lt z-1 size-full overflow-hidden">
<div class="absolute -right-300px -top-900px lt-sm:(-right-100px -top-1170px)">
<svg height="1337" width="1337">
<defs>
<path
id="path-1"
opacity="1"
fill-rule="evenodd"
d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"
/>
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
<stop offset="0" :stop-color="lightColor" stop-opacity="1" />
<stop offset="1" :stop-color="darkColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" />
</g>
</svg>
</div>
<div class="absolute -bottom-400px -left-200px lt-sm:(-bottom-760px -left-100px)">
<svg height="896" width="967.8852157128662">
<defs>
<path
id="path-2"
opacity="1"
fill-rule="evenodd"
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
/>
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
<stop offset="0" :stop-color="darkColor" stop-opacity="1" />
<stop offset="1" :stop-color="lightColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
</g>
</svg>
</div>
</div>
</template>
<style scoped></style>