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,97 +0,0 @@
<template>
<div v-if="reloadFlag" class="relative">
<slot></slot>
<div v-show="showPlaceholder" class="absolute-lt w-full h-full" :class="placeholderClass">
<div v-show="loading" class="absolute-center">
<n-spin :show="true" :size="loadingSize" />
</div>
<div v-show="isEmpty" class="absolute-center">
<div class="relative">
<icon-local-empty-data :class="iconClass" />
<p class="absolute-lb w-full text-center" :class="descClass">{{ emptyDesc }}</p>
</div>
</div>
<div v-show="!network" class="absolute-center">
<div class="relative" :class="{ 'cursor-pointer': showNetworkReload }" @click="handleReload">
<icon-local-network-error :class="iconClass" />
<p class="absolute-lb w-full text-center" :class="descClass">{{ networkErrorDesc }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onUnmounted, watch } from 'vue';
import { NETWORK_ERROR_MSG } from '@/config';
import { useBoolean } from '@/hooks';
defineOptions({ name: 'LoadingEmptyWrapper' });
interface Props {
/** 是否加载 */
loading: boolean;
/** 是否为空 */
empty?: boolean;
/** 加载图标的大小 */
loadingSize?: 'small' | 'medium' | 'large';
/** 中间占位符的class */
placeholderClass?: string;
/** 空数据描述文本 */
emptyDesc?: string;
/** 图标的class */
iconClass?: string;
/** 描述文本的class */
descClass?: string;
/** 显示网络异常的重试点击按钮 */
showNetworkReload?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
empty: false,
loadingSize: 'medium',
placeholderClass: 'bg-white dark:bg-dark transition-background-color duration-300 ease-in-out',
emptyDesc: '暂无数据',
iconClass: 'text-320px text-primary',
descClass: 'text-16px text-#666',
showNetworkReload: false
});
// 网络状态
const { bool: network, setBool: setNetwork } = useBoolean(window.navigator.onLine);
const { bool: reloadFlag, setBool: setReload } = useBoolean(true);
// 数据是否为空
const isEmpty = computed(() => props.empty && !props.loading && network.value);
const showPlaceholder = computed(() => props.loading || isEmpty.value || !network.value);
const networkErrorDesc = computed(() =>
props.showNetworkReload ? `${NETWORK_ERROR_MSG}, 点击重试` : NETWORK_ERROR_MSG
);
function handleReload() {
if (!props.showNetworkReload) return;
setReload(false);
nextTick(() => {
setReload(true);
});
}
const stopHandle = watch(
() => props.loading,
newValue => {
// 结束加载判断一下网络状态
if (!newValue) {
setNetwork(window.navigator.onLine);
}
}
);
onUnmounted(() => {
stopHandle();
});
</script>
<style scoped></style>

View File

@ -1,50 +0,0 @@
<template>
<div class="w-full text-14px">
<n-checkbox v-model:checked="checked">我已经仔细阅读并接受</n-checkbox>
<n-button :text="true" type="primary" @click="handleClickProtocol">用户协议</n-button>
<n-button :text="true" type="primary" @click="handleClickPolicy">隐私权政策</n-button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
defineOptions({ name: 'LoginAgreement' });
interface Props {
/** 是否勾选 */
value?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
value: true
});
interface Emits {
(e: 'update:value', value: boolean): void;
/** 点击协议 */
(e: 'click-protocol'): void;
/** 点击隐私政策 */
(e: 'click-policy'): void;
}
const emit = defineEmits<Emits>();
const checked = computed({
get() {
return props.value;
},
set(newValue: boolean) {
emit('update:value', newValue);
}
});
function handleClickProtocol() {
emit('click-protocol');
}
function handleClickPolicy() {
emit('click-policy');
}
</script>
<style scoped></style>

View File

@ -1,6 +1,30 @@
<script setup lang="ts">
import { getRgbOfColor } from '@sa/utils';
import { $t } from '@/locales';
import { localStg } from '@/utils/storage';
const loadingClasses = [
'left-0 top-0',
'left-0 bottom-0 animate-delay-500',
'right-0 top-0 animate-delay-1000',
'right-0 bottom-0 animate-delay-1500'
];
function addThemeColorCssVars() {
const themeColor = localStg.get('themeColor') || '#646cff';
const { r, g, b } = getRgbOfColor(themeColor);
const cssVars = `--primary-color: ${r} ${g} ${b}`;
document.documentElement.style.cssText = cssVars;
}
addThemeColorCssVars();
</script>
<template>
<div class="fixed-center flex-col">
<system-logo class="text-128px text-primary" />
<SystemLogo class="text-128px text-primary" />
<div class="w-56px h-56px my-36px">
<div class="relative h-full animate-spin">
<div
@ -15,29 +39,4 @@
</div>
</template>
<script setup lang="ts">
import { sessionStg, getRgbOfColor } from '@/utils';
import { $t } from '@/locales';
import themeSettings from '@/settings/theme.json';
const loadingClasses = [
'left-0 top-0',
'left-0 bottom-0 animate-delay-500',
'right-0 top-0 animate-delay-1000',
'right-0 bottom-0 animate-delay-1500'
];
function addThemeColorCssVars() {
const defaultColor = themeSettings.themeColor;
const themeColor = sessionStg.get('themeColor') || defaultColor;
const { r, g, b } = getRgbOfColor(themeColor);
const cssVars = `--primary-color: ${r},${g},${b}`;
document.documentElement.style.cssText = cssVars;
}
addThemeColorCssVars();
</script>
<style scoped></style>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { defineComponent, createTextVNode } from 'vue';
import { useDialog, useLoadingBar, useMessage, useNotification } from 'naive-ui';
defineOptions({
name: 'AppProvider'
});
const ContextHolder = defineComponent({
name: 'ContextHolder',
setup() {
function register() {
window.$loadingBar = useLoadingBar();
window.$dialog = useDialog();
window.$message = useMessage();
window.$notification = useNotification();
}
register();
},
render() {
return createTextVNode();
}
});
</script>
<template>
<NLoadingBarProvider>
<NDialogProvider>
<NNotificationProvider>
<NMessageProvider>
<ContextHolder />
<slot></slot>
</NMessageProvider>
</NNotificationProvider>
</NDialogProvider>
</NLoadingBarProvider>
</template>
<style scoped></style>

View File

@ -1,12 +1,3 @@
<template>
<div
class="dark:bg-dark dark:text-white dark:text-opacity-82 transition-all"
:class="inverted ? 'bg-#001428 text-white' : 'bg-white text-#333639'"
>
<slot></slot>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'DarkModeContainer' });
@ -14,9 +5,13 @@ interface Props {
inverted?: boolean;
}
withDefaults(defineProps<Props>(), {
inverted: false
});
defineProps<Props>();
</script>
<template>
<div class="bg-container text-base_text transition-300" :class="{ 'bg-inverted text-#1f1f1f': inverted }">
<slot></slot>
</div>
</template>
<style scoped></style>

View File

@ -1,89 +0,0 @@
<template>
<div class="flex-center text-18px cursor-pointer" @click="handleSwitch">
<icon-mdi-moon-waning-crescent v-if="darkMode" />
<icon-mdi-white-balance-sunny v-else />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
defineOptions({ name: 'DarkModeSwitch' });
interface Props {
/** 暗黑模式 */
dark?: boolean;
/** 自定义暗黑模式动画过渡 */
customizeTransition?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
dark: false
});
interface Emits {
(e: 'update:dark', darkMode: boolean): void;
}
const emit = defineEmits<Emits>();
const darkMode = computed({
get() {
return props.dark;
},
set(newValue: boolean) {
emit('update:dark', newValue);
}
});
async function handleSwitch(event: MouseEvent) {
const x = event.clientX;
const y = event.clientY;
if (!props.customizeTransition || !document.startViewTransition) {
darkMode.value = !darkMode.value;
return;
}
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
const transition = document.startViewTransition(() => {
darkMode.value = !darkMode.value;
});
await transition.ready;
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`];
document.documentElement.animate(
{
clipPath: darkMode.value ? clipPath : [...clipPath].reverse()
},
{
duration: 300,
easing: 'ease-in',
pseudoElement: darkMode.value ? '::view-transition-new(root)' : '::view-transition-old(root)'
}
);
}
</script>
<style>
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root) {
z-index: 9999;
}
::view-transition-new(root) {
z-index: 1;
}
.dark::view-transition-old(root) {
z-index: 1;
}
.dark::view-transition-new(root) {
z-index: 9999;
}
</style>

View File

@ -1,31 +1,41 @@
<template>
<div class="flex-col-center gap-24px min-h-520px wh-full overflow-hidden">
<div class="flex text-400px text-primary">
<icon-local-no-permission v-if="type === '403'" />
<icon-local-not-found v-if="type === '404'" />
<icon-local-service-error v-if="type === '500'" />
</div>
<router-link :to="{ name: routeHomePath }">
<n-button type="primary">回到首页</n-button>
</router-link>
</div>
</template>
<script lang="ts" setup>
import { routeName } from '@/router';
import { computed } from 'vue';
import { $t } from '@/locales';
defineOptions({ name: 'ExceptionBase' });
type ExceptionType = '403' | '404' | '500';
interface Props {
/** 异常类型 403 404 500 */
/**
* exception type
* - 403: no permission
* - 404: not found
* - 500: service error
*/
type: ExceptionType;
}
defineProps<Props>();
const props = defineProps<Props>();
const routeHomePath = routeName('root');
const iconMap: Record<ExceptionType, string> = {
'403': 'no-permission',
'404': 'not-found',
'500': 'service-error'
};
const icon = computed(() => iconMap[props.type]);
</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="icon" />
</div>
<RouterLink to="/">
<NButton type="primary">{{ $t('common.backToHome') }}</NButton>
</RouterLink>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { $t } from '@/locales';
defineOptions({
name: 'FullScreen'
});
interface Props {
full?: boolean;
}
defineProps<Props>();
</script>
<template>
<ButtonIcon :key="String(full)" :tooltip-content="full ? $t('icon.fullscreenExit') : $t('icon.fullscreen')">
<icon-gridicons-fullscreen-exit v-if="full" />
<icon-gridicons-fullscreen v-else />
</ButtonIcon>
</template>
<style scoped></style>

View File

@ -1,48 +0,0 @@
<template>
<div v-if="showTooltip">
<n-tooltip :placement="placement" trigger="hover">
<template #trigger>
<div class="flex-center h-full cursor-pointer dark:hover:bg-#333" :class="contentClassName">
<slot></slot>
</div>
</template>
{{ tooltipContent }}
</n-tooltip>
</div>
<div v-else class="flex-center cursor-pointer dark:hover:bg-#333" :class="contentClassName">
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import type { PopoverPlacement } from 'naive-ui';
defineOptions({ name: 'HoverContainer' });
interface Props {
/** tooltip显示文本 */
tooltipContent?: string;
/** tooltip的位置 */
placement?: PopoverPlacement;
/** class类 */
contentClass?: string;
/** 反转模式下 */
inverted?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
tooltipContent: '',
placement: 'bottom',
contentClass: '',
inverted: false
});
const showTooltip = computed(() => Boolean(props.tooltipContent));
const contentClassName = computed(
() => `${props.contentClass} ${props.inverted ? 'hover:bg-primary' : 'hover:bg-#f6f6f6'}`
);
</script>
<style scoped></style>

View File

@ -0,0 +1,55 @@
<script setup lang="ts">
import { computed } from 'vue';
import { $t } from '@/locales';
defineOptions({
name: 'LangSwitch'
});
interface Props {
/**
* current language
*/
lang: App.I18n.LangType;
/**
* language options
*/
langOptions: App.I18n.LangOption[];
/**
* show tooltip
*/
showTooltip?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
showTooltip: true
});
type Emits = {
(e: 'changeLang', lang: App.I18n.LangType): void;
};
const emits = defineEmits<Emits>();
const tooltipContent = computed(() => {
if (!props.showTooltip) return '';
return $t('icon.lang');
});
function changeLang(lang: App.I18n.LangType) {
emits('changeLang', lang);
}
</script>
<template>
<NDropdown :value="lang" :options="langOptions" trigger="hover" @select="changeLang">
<div>
<ButtonIcon :tooltip-content="tooltipContent" tooltip-placement="left">
<SvgIcon icon="heroicons:language" />
</ButtonIcon>
</div>
</NDropdown>
</template>
<style scoped></style>

View File

@ -0,0 +1,48 @@
<script lang="ts" setup>
import { $t } from '@/locales';
import { computed } from 'vue';
defineOptions({ name: 'MenuToggler' });
interface Props {
/**
* show collapsed icon
*/
collapsed?: boolean;
/**
* arrow style icon
*/
arrowIcon?: boolean;
}
const props = defineProps<Props>();
type NumberBool = 0 | 1;
const icon = computed(() => {
const icons: Record<NumberBool, Record<NumberBool, string>> = {
0: {
0: 'line-md:menu-fold-left',
1: 'line-md:menu-fold-right'
},
1: {
0: 'ph-caret-double-left-bold',
1: 'ph-caret-double-right-bold'
}
};
const arrowIcon = Number(props.arrowIcon || false) as NumberBool;
const collapsed = Number(props.collapsed || false) as NumberBool;
return icons[arrowIcon][collapsed];
});
</script>
<template>
<ButtonIcon :tooltip-content="collapsed ? $t('icon.expand') : $t('icon.collapse')" tooltip-placement="bottom-start">
<SvgIcon :icon="icon" />
</ButtonIcon>
</template>
<style scoped></style>

View File

@ -1,38 +0,0 @@
<template>
<n-loading-bar-provider>
<n-dialog-provider>
<n-notification-provider>
<n-message-provider>
<slot></slot>
<naive-provider-content />
</n-message-provider>
</n-notification-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</template>
<script setup lang="ts">
import { defineComponent, h } from 'vue';
import { useDialog, useLoadingBar, useMessage, useNotification } from 'naive-ui';
defineOptions({ name: 'NaiveProvider' });
// 挂载naive组件的方法至window, 以便在路由钩子函数和请求函数里面调用
function registerNaiveTools() {
window.$loadingBar = useLoadingBar();
window.$dialog = useDialog();
window.$message = useMessage();
window.$notification = useNotification();
}
const NaiveProviderContent = defineComponent({
name: 'NaiveProviderContent',
setup() {
registerNaiveTools();
},
render() {
return h('div');
}
});
</script>
<style scoped></style>

View File

@ -0,0 +1,26 @@
<script lang="ts" setup>
import { $t } from '@/locales';
import { computed } from 'vue';
defineOptions({ name: 'PinToggler' });
interface Props {
pin?: boolean;
}
const props = defineProps<Props>();
const icon = computed(() => (props.pin ? 'mdi-pin-off' : 'mdi-pin'));
</script>
<template>
<ButtonIcon
:tooltip-content="pin ? $t('icon.pin') : $t('icon.unpin')"
tooltip-placement="bottom-start"
trigger-parent
>
<SvgIcon :icon="icon" />
</ButtonIcon>
</template>
<style scoped></style>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import { $t } from '@/locales';
defineOptions({
name: 'ReloadButton'
});
interface Props {
loading?: boolean;
}
defineProps<Props>();
</script>
<template>
<ButtonIcon :tooltip-content="$t('icon.reload')">
<icon-ant-design-reload-outlined :class="{ 'animate-spin animate-duration-750': loading }" />
</ButtonIcon>
</template>
<style scoped></style>

View File

@ -1,19 +1,9 @@
<template>
<icon-local-logo-fill v-if="fill" />
<icon-local-logo v-else />
</template>
<script lang="ts" setup>
defineOptions({ name: 'SystemLogo' });
interface Props {
/** logo是否填充 */
fill?: boolean;
}
withDefaults(defineProps<Props>(), {
fill: false
});
</script>
<template>
<icon-local-logo />
</template>
<style scoped></style>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { PopoverPlacement } from 'naive-ui';
import { $t } from '@/locales';
defineOptions({ name: 'ThemeSchemaSwitch' });
interface Props {
/**
* theme schema
*/
themeSchema: UnionKey.ThemeScheme;
/**
* show tooltip
*/
showTooltip?: boolean;
/**
* tooltip placement
*/
tooltipPlacement?: PopoverPlacement;
}
const props = withDefaults(defineProps<Props>(), {
showTooltip: true,
tooltipPlacement: 'bottom'
});
interface Emits {
(e: 'switch'): void;
}
const emit = defineEmits<Emits>();
function handleSwitch() {
emit('switch');
}
const icons: Record<UnionKey.ThemeScheme, string> = {
light: 'material-symbols:sunny',
dark: 'material-symbols:nightlight-rounded',
auto: 'material-symbols:hdr-auto'
};
const icon = computed(() => icons[props.themeSchema]);
const tooltipContent = computed(() => {
if (!props.showTooltip) return '';
return $t('icon.themeSchema');
});
</script>
<template>
<ButtonIcon
:icon="icon"
:tooltip-content="tooltipContent"
:tooltip-placement="tooltipPlacement"
@click="handleSwitch"
/>
</template>
<style scoped></style>

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>