feat(projects): 1.0 beta

This commit is contained in:
Soybean
2023-11-17 08:45:00 +08:00
parent 1ea4817f6a
commit e918a2c0f5
499 changed files with 15918 additions and 24708 deletions

View File

@ -1,11 +1,3 @@
<template>
<div ref="bsWrap" class="h-full text-left">
<div ref="bsContent" class="inline-block" :class="{ 'h-full': !isScrollY }">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { useElementSize } from '@vueuse/core';
@ -15,15 +7,21 @@ import type { Options } from '@better-scroll/core';
defineOptions({ name: 'BetterScroll' });
interface Props {
/** better-scroll的配置: https://better-scroll.github.io/docs/zh-CN/guide/base-scroll-options.html */
/**
* BetterScroll options
* @link https://better-scroll.github.io/docs/zh-CN/guide/base-scroll-options.html
*/
options: Options;
}
const props = defineProps<Props>();
const bsWrap = ref<HTMLElement>();
const instance = ref<BScroll>();
const bsContent = ref<HTMLElement>();
const { width: wrapWidth } = useElementSize(bsWrap);
const { width, height } = useElementSize(bsContent);
const instance = ref<BScroll>();
const isScrollY = computed(() => Boolean(props.options.scrollY));
function initBetterScroll() {
@ -31,13 +29,9 @@ function initBetterScroll() {
instance.value = new BScroll(bsWrap.value, props.options);
}
// 滚动元素发生变化刷新BS
const { width: wrapWidth } = useElementSize(bsWrap);
const { width, height } = useElementSize(bsContent);
// refresh BS when scroll element size changed
watch([() => wrapWidth.value, () => width.value, () => height.value], () => {
if (instance.value) {
instance.value.refresh();
}
instance.value?.refresh();
});
onMounted(() => {
@ -47,4 +41,12 @@ onMounted(() => {
defineExpose({ instance });
</script>
<template>
<div ref="bsWrap" 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,86 @@
<script setup lang="ts">
import { computed } from 'vue';
import { createReusableTemplate } from '@vueuse/core';
import type { PopoverPlacement } from 'naive-ui';
defineOptions({
name: 'ButtonIcon',
inheritAttrs: false
});
interface Props {
/**
* button class
*/
class?: string;
/**
* iconify icon name
*/
icon?: string;
/**
* tooltip content
*/
tooltipContent?: string;
/**
* tooltip placement
*/
tooltipPlacement?: PopoverPlacement;
}
const props = withDefaults(defineProps<Props>(), {
class: 'h-36px text-icon',
icon: '',
tooltipContent: '',
tooltipPlacement: 'bottom'
});
interface ButtonProps {
className: string;
}
const [DefineButton, Button] = createReusableTemplate<ButtonProps>();
const cls = computed(() => {
let clsStr = props.class;
if (!clsStr.includes('h-')) {
clsStr += ' h-36px';
}
if (!clsStr.includes('text-')) {
clsStr += ' text-icon';
}
return clsStr;
});
</script>
<template>
<!-- define component: Button -->
<DefineButton v-slot="{ $slots, className }">
<NButton quaternary :class="className">
<div class="flex-center gap-8px">
<component :is="$slots.default" />
</div>
</NButton>
</DefineButton>
<!-- template -->
<NTooltip v-if="tooltipContent" :placement="tooltipPlacement">
<template #trigger>
<Button :class-name="cls" v-bind="$attrs">
<slot>
<SvgIcon :icon="icon" />
</slot>
</Button>
</template>
{{ tooltipContent }}
</NTooltip>
<Button v-else :class-name="cls" v-bind="$attrs">
<slot>
<SvgIcon :icon="icon" />
</slot>
</Button>
</template>
<style scoped></style>

View File

@ -1,116 +0,0 @@
<template>
<span>{{ value }}</span>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch, watchEffect } from 'vue';
import { TransitionPresets, useTransition } from '@vueuse/core';
import { isNumber } from '@/utils';
defineOptions({ name: 'CountTo' });
type TransitionKey = keyof typeof TransitionPresets;
interface Props {
/** 初始值 */
startValue?: number;
/** 结束值 */
endValue?: number;
/** 动画时长 */
duration?: number;
/** 自动动画 */
autoplay?: boolean;
/** 进制 */
decimals?: number;
/** 前缀 */
prefix?: string;
/** 后缀 */
suffix?: string;
/** 分割符号 */
separator?: string;
/** 小数点 */
decimal?: string;
/** 使用缓冲动画函数 */
useEasing?: boolean;
/** 缓冲动画函数类型 */
transition?: TransitionKey;
}
const props = withDefaults(defineProps<Props>(), {
startValue: 0,
endValue: 2021,
duration: 1500,
autoplay: true,
decimals: 0,
prefix: '',
suffix: '',
separator: ',',
decimal: '.',
useEasing: true,
transition: 'linear'
});
interface Emits {
(e: 'on-started'): void;
(e: 'on-finished'): void;
}
const emit = defineEmits<Emits>();
const source = ref(props.startValue);
let outputValue = useTransition(source);
const value = computed(() => formatNumber(outputValue.value));
const disabled = ref(false);
function run() {
outputValue = useTransition(source, {
disabled,
duration: props.duration,
onStarted: () => emit('on-started'),
onFinished: () => emit('on-finished'),
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {})
});
}
function start() {
run();
source.value = props.endValue;
}
function formatNumber(num: number | string) {
if (num !== 0 && !num) {
return '';
}
const { decimals, decimal, separator, suffix, prefix } = props;
let number = 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 && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, `$1${separator}$2`);
}
}
return prefix + x1 + x2 + suffix;
}
watch([() => props.startValue, () => props.endValue], () => {
if (props.autoplay) {
start();
}
});
watchEffect(() => {
source.value = props.startValue;
});
onMounted(() => {
if (props.autoplay) {
start();
}
});
</script>
<style scoped></style>

View File

@ -1,17 +0,0 @@
<template>
<web-site-link label="github地址" :link="link" />
</template>
<script setup lang="ts">
import WebSiteLink from './web-site-link.vue';
defineOptions({ name: 'GithubLink' });
interface Props {
/** github链接 */
link: string;
}
defineProps<Props>();
</script>
<style scoped></style>

View File

@ -1,77 +0,0 @@
<template>
<n-popover placement="bottom-end" trigger="click">
<template #trigger>
<n-input v-model:value="modelValue" readonly placeholder="点击选择图标">
<template #suffix>
<svg-icon :icon="selectedIcon" class="text-30px p-5px" />
</template>
</n-input>
</template>
<template #header>
<n-input v-model:value="searchValue" placeholder="搜索图标"></n-input>
</template>
<div v-if="iconsList.length > 0" class="grid grid-cols-9 h-auto overflow-auto">
<span v-for="iconItem in iconsList" :key="iconItem" @click="handleChange(iconItem)">
<svg-icon
:icon="iconItem"
class="border-1px border-#d9d9d9 text-30px m-2px p-5px cursor-pointer"
:class="{ 'border-primary': modelValue === iconItem }"
/>
</span>
</div>
<n-empty v-else class="w-306px" description="你什么也找不到" />
</n-popover>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
defineOptions({ name: 'IconSelect' });
interface Props {
/** 选中的图标 */
value: string;
/** 图标列表 */
icons: string[];
/** 未选中图标 */
emptyIcon?: string;
}
const props = withDefaults(defineProps<Props>(), {
emptyIcon: 'mdi:apps'
});
interface Emits {
(e: 'update:value', val: string): void;
}
const emit = defineEmits<Emits>();
const modelValue = computed({
get() {
return props.value;
},
set(val: string) {
emit('update:value', val);
}
});
const selectedIcon = computed(() => modelValue.value || props.emptyIcon);
const searchValue = ref('');
const iconsList = computed(() => props.icons.filter(v => v.includes(searchValue.value)));
function handleChange(iconItem: string) {
modelValue.value = iconItem;
}
</script>
<style lang="scss" scoped>
:deep(.n-input-wrapper) {
padding-right: 0;
}
:deep(.n-input__suffix) {
border: 1px solid #d9d9d9;
}
</style>

View File

@ -1,42 +0,0 @@
<template>
<div>
<canvas ref="domRef" width="152" height="40" class="cursor-pointer" @click="getImgCode"></canvas>
</div>
</template>
<script setup lang="ts">
import { watch } from 'vue';
import { useImageVerify } from '@/hooks';
defineOptions({ name: 'ImageVerify' });
interface Props {
code?: string;
}
const props = withDefaults(defineProps<Props>(), {
code: ''
});
interface Emits {
(e: 'update:code', code: string): void;
}
const emit = defineEmits<Emits>();
const { domRef, imgCode, setImgCode, getImgCode } = useImageVerify();
watch(
() => props.code,
newValue => {
setImgCode(newValue);
}
);
watch(imgCode, newValue => {
emit('update:code', newValue);
});
defineExpose({ getImgCode });
</script>
<style scoped></style>

View File

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

View File

@ -1,14 +1,3 @@
<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>
<script setup lang="ts">
import { computed, useAttrs } from 'vue';
import { Icon } from '@iconify/vue';
@ -16,14 +5,18 @@ import { Icon } from '@iconify/vue';
defineOptions({ name: 'SvgIcon' });
/**
* 图标组件
* - 支持iconify和本地svg图标
* - 同时传递了icon和localIconlocalIcon会优先渲染
* 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;
/** 本地svg的文件名 */
/**
* local svg icon name
*/
localIcon?: string;
}
@ -46,8 +39,21 @@ const symbolId = computed(() => {
return `#${prefix}-${icon}`;
});
/** 渲染本地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 { getColorPalette } from '@sa/utils';
interface Props {
/**
* theme color
*/
themeColor: string;
}
const props = defineProps<Props>();
const lightColor = computed(() => getColorPalette(props.themeColor, 3));
const darkColor = computed(() => getColorPalette(props.themeColor, 6));
</script>
<template>
<div class="absolute-lt z-1 wh-full overflow-hidden">
<div class="absolute -right-300px -top-900px <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 -left-200px -bottom-400px <sm:(-left-100px -bottom-760px)">
<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>

View File

@ -1,23 +0,0 @@
<template>
<p>
<span>{{ label }}</span>
<a class="text-blue-500" :href="link" target="_blank">
{{ link }}
</a>
</p>
</template>
<script setup lang="ts">
defineOptions({ name: 'WebSiteLink' });
interface Props {
/** 网址名称 */
label: string;
/** 网址链接 */
link: string;
}
defineProps<Props>();
</script>
<style scoped></style>