初始化
This commit is contained in:
53
src/components/custom/better-scroll.vue
Normal file
53
src/components/custom/better-scroll.vue
Normal 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>
|
||||
118
src/components/custom/business-form-dialog.vue
Normal file
118
src/components/custom/business-form-dialog.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'BusinessFormDialog',
|
||||
inheritAttrs: false
|
||||
});
|
||||
|
||||
type DialogPreset = 'sm' | 'md' | 'lg';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
preset?: DialogPreset;
|
||||
width?: string | number;
|
||||
loading?: boolean;
|
||||
confirmLoading?: boolean;
|
||||
confirmDisabled?: boolean;
|
||||
showFooter?: boolean;
|
||||
scrollbar?: boolean;
|
||||
maxBodyHeight?: string | number;
|
||||
cancelText?: string;
|
||||
confirmText?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'cancel'): void;
|
||||
(e: 'confirm'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
preset: 'md',
|
||||
width: undefined,
|
||||
loading: false,
|
||||
confirmLoading: false,
|
||||
confirmDisabled: false,
|
||||
showFooter: true,
|
||||
scrollbar: true,
|
||||
maxBodyHeight: '70vh',
|
||||
cancelText: '',
|
||||
confirmText: ''
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>({
|
||||
default: false
|
||||
});
|
||||
|
||||
const DIALOG_WIDTH_MAP: Record<DialogPreset, string> = {
|
||||
sm: '520px',
|
||||
md: '640px',
|
||||
lg: '720px'
|
||||
};
|
||||
|
||||
const dialogWidth = computed(() => props.width ?? DIALOG_WIDTH_MAP[props.preset]);
|
||||
|
||||
const resolvedCancelText = computed(() => props.cancelText || $t('common.cancel'));
|
||||
const resolvedConfirmText = computed(() => props.confirmText || $t('common.confirm'));
|
||||
|
||||
function handleCancel() {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
emit('confirm');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="visible"
|
||||
class="business-form-dialog"
|
||||
:title="props.title"
|
||||
:width="dialogWidth"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
align-center
|
||||
header-class="business-form-dialog__header"
|
||||
body-class="business-form-dialog__body"
|
||||
footer-class="business-form-dialog__footer"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<ElScrollbar
|
||||
v-if="props.scrollbar"
|
||||
:max-height="props.maxBodyHeight"
|
||||
class="business-form-dialog__scrollbar"
|
||||
view-class="business-form-dialog__scrollbar-view"
|
||||
>
|
||||
<div v-loading="props.loading" class="business-form-dialog__content">
|
||||
<slot />
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
|
||||
<div v-else v-loading="props.loading" class="business-form-dialog__content">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<template v-if="props.showFooter" #footer>
|
||||
<slot name="footer" :close="handleCancel">
|
||||
<ElSpace :size="16">
|
||||
<ElButton @click="handleCancel">{{ resolvedCancelText }}</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="props.confirmLoading"
|
||||
:disabled="props.confirmDisabled"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ resolvedConfirmText }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</slot>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
117
src/components/custom/business-form-drawer.vue
Normal file
117
src/components/custom/business-form-drawer.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'BusinessFormDrawer',
|
||||
inheritAttrs: false
|
||||
});
|
||||
|
||||
type DrawerPreset = 'md' | 'lg' | 'xl';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
preset?: DrawerPreset;
|
||||
size?: string | number;
|
||||
loading?: boolean;
|
||||
confirmLoading?: boolean;
|
||||
confirmDisabled?: boolean;
|
||||
showFooter?: boolean;
|
||||
scrollbar?: boolean;
|
||||
bodyHeight?: string | number;
|
||||
cancelText?: string;
|
||||
confirmText?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'cancel'): void;
|
||||
(e: 'confirm'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
preset: 'lg',
|
||||
size: undefined,
|
||||
loading: false,
|
||||
confirmLoading: false,
|
||||
confirmDisabled: false,
|
||||
showFooter: true,
|
||||
scrollbar: true,
|
||||
bodyHeight: '100%',
|
||||
cancelText: '',
|
||||
confirmText: ''
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>({
|
||||
default: false
|
||||
});
|
||||
|
||||
const DRAWER_SIZE_MAP: Record<DrawerPreset, string> = {
|
||||
md: '480px',
|
||||
lg: '720px',
|
||||
xl: '960px'
|
||||
};
|
||||
|
||||
const drawerSize = computed(() => props.size ?? DRAWER_SIZE_MAP[props.preset]);
|
||||
|
||||
const resolvedCancelText = computed(() => props.cancelText || $t('common.cancel'));
|
||||
const resolvedConfirmText = computed(() => props.confirmText || $t('common.confirm'));
|
||||
|
||||
function handleCancel() {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
emit('confirm');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDrawer
|
||||
v-model="visible"
|
||||
class="business-form-drawer"
|
||||
:title="props.title"
|
||||
:size="drawerSize"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
header-class="business-form-drawer__header"
|
||||
body-class="business-form-drawer__body"
|
||||
footer-class="business-form-drawer__footer"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<ElScrollbar
|
||||
v-if="props.scrollbar"
|
||||
:height="props.bodyHeight"
|
||||
class="business-form-drawer__scrollbar"
|
||||
view-class="business-form-drawer__scrollbar-view"
|
||||
>
|
||||
<div v-loading="props.loading" class="business-form-drawer__content">
|
||||
<slot />
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
|
||||
<div v-else v-loading="props.loading" class="business-form-drawer__content">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<template v-if="props.showFooter" #footer>
|
||||
<slot name="footer" :close="handleCancel">
|
||||
<ElSpace :size="16">
|
||||
<ElButton @click="handleCancel">{{ resolvedCancelText }}</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="props.confirmLoading"
|
||||
:disabled="props.confirmDisabled"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ resolvedConfirmText }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</slot>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
20
src/components/custom/business-form-section.vue
Normal file
20
src/components/custom/business-form-section.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'BusinessFormSection'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="business-form-section">
|
||||
<h4 class="business-form-section__title">{{ title }}</h4>
|
||||
<slot />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
111
src/components/custom/business-table-action-cell.tsx
Normal file
111
src/components/custom/business-table-action-cell.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { ElButton, ElPopover } from 'element-plus';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
export type BusinessTableAction = {
|
||||
key: string;
|
||||
label: string;
|
||||
buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||
disabled?: boolean;
|
||||
onClick: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BusinessTableActionCell',
|
||||
props: {
|
||||
actions: {
|
||||
type: Array as PropType<BusinessTableAction[]>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const popoverVisible = ref(false);
|
||||
|
||||
const directActions = computed(() => {
|
||||
if (props.actions.length <= 2) {
|
||||
return props.actions;
|
||||
}
|
||||
|
||||
return props.actions.slice(0, 1);
|
||||
});
|
||||
|
||||
const moreActions = computed(() => {
|
||||
if (props.actions.length <= 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return props.actions.slice(1);
|
||||
});
|
||||
|
||||
async function handleAction(action: BusinessTableAction) {
|
||||
if (action.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
popoverVisible.value = false;
|
||||
await action.onClick();
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class="business-table-action-cell" onClick={event => event.stopPropagation()}>
|
||||
{directActions.value.map(action => (
|
||||
<ElButton
|
||||
key={action.key}
|
||||
plain
|
||||
size="small"
|
||||
type={action.buttonType}
|
||||
disabled={action.disabled}
|
||||
class="business-table-action-button"
|
||||
onClick={() => handleAction(action)}
|
||||
>
|
||||
{action.label}
|
||||
</ElButton>
|
||||
))}
|
||||
|
||||
{moreActions.value.length > 0 && (
|
||||
<ElPopover
|
||||
v-model:visible={popoverVisible.value}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
width={120}
|
||||
show-arrow={false}
|
||||
>
|
||||
{{
|
||||
reference: () => (
|
||||
<ElButton
|
||||
plain
|
||||
size="small"
|
||||
class="business-table-action-button"
|
||||
onClick={event => event.stopPropagation()}
|
||||
>
|
||||
<span class="inline-flex items-center gap-4px">
|
||||
{$t('common.more')}
|
||||
<icon-mdi-chevron-down class="text-14px" />
|
||||
</span>
|
||||
</ElButton>
|
||||
),
|
||||
default: () => (
|
||||
<div class="business-table-action-menu">
|
||||
{moreActions.value.map(action => (
|
||||
<ElButton
|
||||
key={action.key}
|
||||
plain
|
||||
size="small"
|
||||
type={action.buttonType}
|
||||
disabled={action.disabled}
|
||||
class="business-table-action-menu__button"
|
||||
onClick={() => handleAction(action)}
|
||||
>
|
||||
{action.label}
|
||||
</ElButton>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</ElPopover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
45
src/components/custom/button-icon.vue
Normal file
45
src/components/custom/button-icon.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { Placement } from 'element-plus';
|
||||
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?: Placement;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
class: '',
|
||||
icon: '',
|
||||
tooltipContent: '',
|
||||
tooltipPlacement: 'bottom',
|
||||
zIndex: 98
|
||||
});
|
||||
|
||||
const DEFAULT_CLASS = 'h-[36px] text-icon';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElTooltip :placement="tooltipPlacement" :content="tooltipContent" :z-index="zIndex" :disabled="!tooltipContent">
|
||||
<ElButton text quaternary :class="twMerge(DEFAULT_CLASS, props.class)" v-bind="$attrs">
|
||||
<div class="flex-center gap-8px text-lg">
|
||||
<slot>
|
||||
<SvgIcon :icon="icon" />
|
||||
</slot>
|
||||
</div>
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
88
src/components/custom/count-to.vue
Normal file
88
src/components/custom/count-to.vue
Normal 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>
|
||||
239
src/components/custom/custom-icon-select.vue
Normal file
239
src/components/custom/custom-icon-select.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
defineOptions({ name: 'CustomIconSelect' });
|
||||
|
||||
interface IconGroup {
|
||||
label: string;
|
||||
value: string;
|
||||
icons: string[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
icons?: string[];
|
||||
iconGroups?: IconGroup[];
|
||||
emptyIcon?: string;
|
||||
title?: string;
|
||||
placeholder?: string;
|
||||
emptyDescription?: string;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
icons: () => [],
|
||||
iconGroups: () => [],
|
||||
emptyIcon: 'mdi:apps',
|
||||
title: '选择图标',
|
||||
placeholder: '点击选择图标',
|
||||
emptyDescription: '没有找到可选图标',
|
||||
pageSize: 48
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', val: string): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const activeGroup = ref('all');
|
||||
const currentPage = ref(1);
|
||||
|
||||
const modelValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val: string) {
|
||||
emit('update:value', val);
|
||||
}
|
||||
});
|
||||
|
||||
const selectedIcon = computed(() => modelValue.value || props.emptyIcon);
|
||||
|
||||
const normalizedGroups = computed<IconGroup[]>(() => {
|
||||
if (props.iconGroups.length) {
|
||||
const groups = props.iconGroups.map(group => ({
|
||||
...group,
|
||||
icons: Array.from(new Set(group.icons))
|
||||
}));
|
||||
|
||||
return [
|
||||
{
|
||||
label: '全部',
|
||||
value: 'all',
|
||||
icons: Array.from(new Set(groups.flatMap(group => group.icons)))
|
||||
},
|
||||
...groups
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: '全部',
|
||||
value: 'all',
|
||||
icons: Array.from(new Set(props.icons))
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
const currentGroup = computed(
|
||||
() => normalizedGroups.value.find(group => group.value === activeGroup.value) ?? normalizedGroups.value[0]
|
||||
);
|
||||
|
||||
const groupIcons = computed(() => currentGroup.value?.icons ?? []);
|
||||
|
||||
const pagedIcons = computed(() => {
|
||||
const startIndex = (currentPage.value - 1) * props.pageSize;
|
||||
|
||||
return groupIcons.value.slice(startIndex, startIndex + props.pageSize);
|
||||
});
|
||||
|
||||
watch(
|
||||
normalizedGroups,
|
||||
groups => {
|
||||
if (!groups.some(group => group.value === activeGroup.value)) {
|
||||
activeGroup.value = groups[0]?.value ?? 'all';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(activeGroup, () => {
|
||||
currentPage.value = 1;
|
||||
});
|
||||
|
||||
function openDialog() {
|
||||
activeGroup.value = normalizedGroups.value[0]?.value ?? 'all';
|
||||
currentPage.value = 1;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleChange(iconItem: string) {
|
||||
modelValue.value = iconItem;
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="custom-icon-select">
|
||||
<div class="custom-icon-select__trigger" @click="openDialog">
|
||||
<ElInput :model-value="modelValue" readonly :placeholder="placeholder">
|
||||
<template #suffix>
|
||||
<SvgIcon :icon="selectedIcon" class="text-22px" />
|
||||
</template>
|
||||
</ElInput>
|
||||
</div>
|
||||
|
||||
<BusinessFormDialog
|
||||
v-model="dialogVisible"
|
||||
:title="title"
|
||||
preset="lg"
|
||||
width="720px"
|
||||
:show-footer="false"
|
||||
:scrollbar="false"
|
||||
>
|
||||
<div class="custom-icon-select__content">
|
||||
<ElTabs v-model="activeGroup" class="custom-icon-select__tabs">
|
||||
<ElTabPane
|
||||
v-for="group in normalizedGroups"
|
||||
:key="group.value"
|
||||
:label="`${group.label} (${group.icons.length})`"
|
||||
:name="group.value"
|
||||
/>
|
||||
</ElTabs>
|
||||
|
||||
<div v-if="pagedIcons.length" class="custom-icon-select__grid">
|
||||
<button
|
||||
v-for="iconItem in pagedIcons"
|
||||
:key="iconItem"
|
||||
type="button"
|
||||
class="custom-icon-select__item"
|
||||
:class="{ 'custom-icon-select__item--active': modelValue === iconItem }"
|
||||
:title="iconItem"
|
||||
@click="handleChange(iconItem)"
|
||||
>
|
||||
<SvgIcon :icon="iconItem" class="text-24px" />
|
||||
<span class="custom-icon-select__name">{{ iconItem }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<ElEmpty v-else :description="emptyDescription" class="py-24px" />
|
||||
|
||||
<div v-if="groupIcons.length > props.pageSize" class="custom-icon-select__pagination">
|
||||
<ElPagination
|
||||
v-model:current-page="currentPage"
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:page-size="props.pageSize"
|
||||
:total="groupIcons.length"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BusinessFormDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-icon-select__trigger {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-icon-select__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.custom-icon-select__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
min-height: 360px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.custom-icon-select__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--el-fill-color-blank);
|
||||
padding: 12px 8px;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.custom-icon-select__item:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.custom-icon-select__item--active {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-5) inset;
|
||||
}
|
||||
|
||||
.custom-icon-select__name {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.custom-icon-select__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 4px;
|
||||
}
|
||||
</style>
|
||||
18
src/components/custom/github-link.vue
Normal file
18
src/components/custom/github-link.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import WebSiteLink from './web-site-link.vue';
|
||||
|
||||
defineOptions({ name: 'GithubLink' });
|
||||
|
||||
interface Props {
|
||||
/** github link */
|
||||
link: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WebSiteLink label="github地址:" :link="link" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
18
src/components/custom/look-forward.vue
Normal file
18
src/components/custom/look-forward.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<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>
|
||||
9
src/components/custom/soybean-avatar.vue
Normal file
9
src/components/custom/soybean-avatar.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'SoybeanAvatar' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="size-72px flex-center rd-1/2 bg-primary text-24px text-white font-600">CN</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
54
src/components/custom/svg-icon.vue
Normal file
54
src/components/custom/svg-icon.vue
Normal 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>
|
||||
163
src/components/custom/table-search-panel.vue
Normal file
163
src/components/custom/table-search-panel.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useSlots } from 'vue';
|
||||
import type { ComponentPublicInstance } from 'vue';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'TableSearchPanel' });
|
||||
|
||||
interface Props {
|
||||
model: object;
|
||||
rules?: Record<string, App.Global.FormRule | App.Global.FormRule[]>;
|
||||
formRef?: ((instance: FormInstance | null) => void) | null;
|
||||
labelWidth?: string | number;
|
||||
labelPosition?: 'left' | 'right' | 'top';
|
||||
gutter?: number;
|
||||
actionColLg?: number;
|
||||
actionColMd?: number;
|
||||
actionColSm?: number;
|
||||
disabled?: boolean;
|
||||
defaultExpanded?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
rules: undefined,
|
||||
formRef: null,
|
||||
labelWidth: 80,
|
||||
labelPosition: 'right',
|
||||
gutter: 24,
|
||||
actionColLg: 6,
|
||||
actionColMd: 24,
|
||||
actionColSm: 24,
|
||||
disabled: false,
|
||||
defaultExpanded: false
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const slots = useSlots();
|
||||
const expanded = ref(props.defaultExpanded);
|
||||
|
||||
const hasExtra = computed(() => Boolean(slots.extra));
|
||||
|
||||
function handleReset() {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
emit('search');
|
||||
}
|
||||
|
||||
function toggleExpanded() {
|
||||
if (!hasExtra.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
expanded.value = !expanded.value;
|
||||
}
|
||||
|
||||
function handleFormRef(instance: Element | ComponentPublicInstance | null) {
|
||||
props.formRef?.(instance as FormInstance | null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCard class="card-wrapper">
|
||||
<ElForm
|
||||
:ref="handleFormRef"
|
||||
:model="props.model"
|
||||
:rules="props.rules"
|
||||
:label-position="props.labelPosition"
|
||||
:label-width="props.labelWidth"
|
||||
@submit.prevent
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<ElRow :gutter="props.gutter">
|
||||
<slot />
|
||||
|
||||
<ElCol
|
||||
v-if="!hasExtra"
|
||||
class="table-search-panel__action-col"
|
||||
:lg="props.actionColLg"
|
||||
:md="props.actionColMd"
|
||||
:sm="props.actionColSm"
|
||||
>
|
||||
<ElSpace class="w-full justify-end" alignment="end">
|
||||
<ElButton :disabled="props.disabled" @click="handleReset">
|
||||
<template #icon>
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.reset') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" plain :disabled="props.disabled" @click="handleSearch">
|
||||
<template #icon>
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.search') }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow v-if="hasExtra && expanded" :gutter="props.gutter">
|
||||
<slot name="extra" />
|
||||
|
||||
<ElCol
|
||||
class="table-search-panel__action-col"
|
||||
:lg="props.actionColLg"
|
||||
:md="props.actionColMd"
|
||||
:sm="props.actionColSm"
|
||||
>
|
||||
<ElSpace class="w-full justify-end" alignment="end">
|
||||
<ElButton circle :disabled="props.disabled" @click="toggleExpanded">
|
||||
<icon-mdi-chevron-double-up class="text-16px" />
|
||||
</ElButton>
|
||||
<ElButton :disabled="props.disabled" @click="handleReset">
|
||||
<template #icon>
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.reset') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" plain :disabled="props.disabled" @click="handleSearch">
|
||||
<template #icon>
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.search') }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<div v-if="hasExtra && !expanded" class="flex justify-end">
|
||||
<ElSpace>
|
||||
<ElButton circle :disabled="props.disabled" @click="toggleExpanded">
|
||||
<icon-mdi-chevron-double-down class="text-16px" />
|
||||
</ElButton>
|
||||
<ElButton :disabled="props.disabled" @click="handleReset">
|
||||
<template #icon>
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.reset') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" plain :disabled="props.disabled" @click="handleSearch">
|
||||
<template #icon>
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.search') }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-search-panel__action-col {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
648
src/components/custom/wave-bg.vue
Normal file
648
src/components/custom/wave-bg.vue
Normal file
@@ -0,0 +1,648 @@
|
||||
<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 accentColor = computed(() => getPaletteColorByNumber(props.themeColor, 300));
|
||||
const darkColor = computed(() => getPaletteColorByNumber(props.themeColor, 500));
|
||||
const deepColor = computed(() => getPaletteColorByNumber(props.themeColor, 700));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="power-bg pointer-events-none absolute-lt z-1 size-full overflow-hidden"
|
||||
:style="{
|
||||
'--power-light': lightColor,
|
||||
'--power-accent': accentColor,
|
||||
'--power-dark': darkColor,
|
||||
'--power-deep': deepColor
|
||||
}"
|
||||
>
|
||||
<div class="power-bg__base"></div>
|
||||
<div class="power-bg__aurora power-bg__aurora--left"></div>
|
||||
<div class="power-bg__aurora power-bg__aurora--right"></div>
|
||||
<div class="power-bg__curtain power-bg__curtain--left"></div>
|
||||
<div class="power-bg__curtain power-bg__curtain--right"></div>
|
||||
<div class="power-bg__grid"></div>
|
||||
<div class="power-bg__scan"></div>
|
||||
<div class="power-bg__halo power-bg__halo--top"></div>
|
||||
<div class="power-bg__halo power-bg__halo--bottom"></div>
|
||||
<div class="power-bg__particles">
|
||||
<span class="power-bg__particle power-bg__particle--1"></span>
|
||||
<span class="power-bg__particle power-bg__particle--2"></span>
|
||||
<span class="power-bg__particle power-bg__particle--3"></span>
|
||||
<span class="power-bg__particle power-bg__particle--4"></span>
|
||||
<span class="power-bg__particle power-bg__particle--5"></span>
|
||||
<span class="power-bg__particle power-bg__particle--6"></span>
|
||||
</div>
|
||||
|
||||
<div class="power-bg__blob power-bg__blob--top">
|
||||
<svg height="1337" width="1337" viewBox="0 0 1337 1337">
|
||||
<defs>
|
||||
<path
|
||||
id="power-blob-top"
|
||||
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 0,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,0 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"
|
||||
/>
|
||||
<linearGradient id="power-gradient-top" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
|
||||
<stop offset="0" :stop-color="lightColor" stop-opacity="0.52" />
|
||||
<stop offset="1" :stop-color="darkColor" stop-opacity="0.16" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<use xlink:href="#power-blob-top" fill="url(#power-gradient-top)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="power-bg__blob power-bg__blob--bottom">
|
||||
<svg height="896" width="968" viewBox="0 0 968 896">
|
||||
<defs>
|
||||
<path
|
||||
id="power-blob-bottom"
|
||||
fill-rule="evenodd"
|
||||
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 0,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,0 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
|
||||
/>
|
||||
<linearGradient id="power-gradient-bottom" x1="0.5" y1="0" x2="0.5" y2="1">
|
||||
<stop offset="0" :stop-color="darkColor" stop-opacity="0.18" />
|
||||
<stop offset="1" :stop-color="lightColor" stop-opacity="0.5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<use xlink:href="#power-blob-bottom" fill="url(#power-gradient-bottom)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<svg class="power-bg__panel" viewBox="0 0 1440 900" preserveAspectRatio="none" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="power-arc-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" :stop-color="lightColor" stop-opacity="0.42" />
|
||||
<stop offset="45%" :stop-color="accentColor" stop-opacity="0.22" />
|
||||
<stop offset="100%" :stop-color="deepColor" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="power-dash-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" :stop-color="lightColor" stop-opacity="0" />
|
||||
<stop offset="50%" :stop-color="lightColor" stop-opacity="0.52" />
|
||||
<stop offset="100%" :stop-color="accentColor" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="power-skyline-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" :stop-color="lightColor" stop-opacity="0.16" />
|
||||
<stop offset="100%" :stop-color="deepColor" stop-opacity="0.02" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<g class="power-bg__panel-arcs">
|
||||
<path d="M-120 770 C180 570, 430 562, 706 650 S1162 890, 1568 778" />
|
||||
<path d="M90 120 C368 -4, 706 -2, 988 124 S1362 368, 1536 314" />
|
||||
<path d="M804 -80 C1012 92, 1130 236, 1160 442 S1110 786, 948 990" />
|
||||
<path d="M28 520 C214 444, 380 426, 592 468 S950 618, 1188 592 1380 510, 1502 438" />
|
||||
</g>
|
||||
|
||||
<g class="power-bg__panel-dashes">
|
||||
<path d="M104 214 H324" />
|
||||
<path d="M104 242 H248" />
|
||||
<path d="M1078 180 H1298" />
|
||||
<path d="M1078 208 H1368" />
|
||||
<path d="M1118 724 H1328" />
|
||||
</g>
|
||||
|
||||
<g class="power-bg__skyline">
|
||||
<path
|
||||
d="M0 900 V758 H94 V724 H142 V686 H196 V730 H258 V654 H318 V698 H364 V622 H422 V746 H486 V670 H554 V714 H612 V636 H682 V728 H746 V592 H804 V656 H852 V618 H910 V744 H978 V668 H1042 V710 H1100 V632 H1166 V692 H1236 V606 H1296 V744 H1362 V694 H1410 V760 H1440 V900 Z"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<g class="power-bg__panel-rings">
|
||||
<circle cx="1148" cy="642" r="96" />
|
||||
<circle cx="1148" cy="642" r="156" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<svg class="power-bg__network" viewBox="0 0 1440 900" preserveAspectRatio="none" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="power-line-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" :stop-color="deepColor" stop-opacity="0" />
|
||||
<stop offset="35%" :stop-color="accentColor" stop-opacity="0.16" />
|
||||
<stop offset="65%" :stop-color="lightColor" stop-opacity="0.32" />
|
||||
<stop offset="100%" :stop-color="deepColor" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="power-flow-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" :stop-color="accentColor" stop-opacity="0" />
|
||||
<stop offset="50%" :stop-color="lightColor" stop-opacity="0.7" />
|
||||
<stop offset="100%" :stop-color="accentColor" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<filter id="power-glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="4" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<g class="power-bg__lines">
|
||||
<path d="M50 620 C220 590, 260 520, 420 530 S710 610, 890 540 1170 430, 1390 470" />
|
||||
<path d="M120 710 C280 660, 390 680, 560 630 S840 500, 1010 530 1240 650, 1410 610" />
|
||||
<path d="M180 250 C330 210, 420 260, 580 240 S860 150, 1010 180 1210 320, 1370 280" />
|
||||
<path d="M310 210 L310 576" />
|
||||
<path d="M780 178 L780 618" />
|
||||
<path d="M1188 164 L1188 648" />
|
||||
</g>
|
||||
|
||||
<g class="power-bg__flows">
|
||||
<path d="M50 620 C220 590, 260 520, 420 530 S710 610, 890 540 1170 430, 1390 470" />
|
||||
<path d="M120 710 C280 660, 390 680, 560 630 S840 500, 1010 530 1240 650, 1410 610" />
|
||||
<path d="M180 250 C330 210, 420 260, 580 240 S860 150, 1010 180 1210 320, 1370 280" />
|
||||
</g>
|
||||
|
||||
<g class="power-bg__nodes" filter="url(#power-glow)">
|
||||
<circle cx="240" cy="575" r="4" />
|
||||
<circle cx="430" cy="530" r="5" />
|
||||
<circle cx="700" cy="585" r="4" />
|
||||
<circle cx="1015" cy="530" r="5" />
|
||||
<circle cx="1180" cy="446" r="4" />
|
||||
<circle cx="355" cy="230" r="3" />
|
||||
<circle cx="825" cy="176" r="3" />
|
||||
<circle cx="310" cy="576" r="4" />
|
||||
<circle cx="780" cy="618" r="4" />
|
||||
<circle cx="1188" cy="648" r="6" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.power-bg {
|
||||
--power-light: #8fd4ff;
|
||||
--power-accent: #4da8ff;
|
||||
--power-dark: #2765d2;
|
||||
--power-deep: #173a82;
|
||||
}
|
||||
|
||||
.power-bg__base,
|
||||
.power-bg__aurora,
|
||||
.power-bg__curtain,
|
||||
.power-bg__grid,
|
||||
.power-bg__scan,
|
||||
.power-bg__halo,
|
||||
.power-bg__particles,
|
||||
.power-bg__panel,
|
||||
.power-bg__network,
|
||||
.power-bg__blob {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.power-bg__base {
|
||||
background:
|
||||
radial-gradient(circle at 18% 22%, color-mix(in srgb, var(--power-light) 26%, transparent) 0, transparent 32%),
|
||||
radial-gradient(circle at 82% 16%, color-mix(in srgb, var(--power-accent) 20%, transparent) 0, transparent 28%),
|
||||
radial-gradient(circle at 74% 72%, color-mix(in srgb, var(--power-dark) 14%, transparent) 0, transparent 34%),
|
||||
linear-gradient(135deg, rgb(255 255 255 / 20%) 0, transparent 28%, transparent 72%, rgb(255 255 255 / 8%) 100%);
|
||||
animation: powerPulse 16s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.power-bg__aurora {
|
||||
border-radius: 50%;
|
||||
filter: blur(42px);
|
||||
opacity: 0.42;
|
||||
mix-blend-mode: screen;
|
||||
animation: powerAurora 18s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.power-bg__aurora--left {
|
||||
top: 8%;
|
||||
left: -10%;
|
||||
width: 42%;
|
||||
height: 52%;
|
||||
background:
|
||||
linear-gradient(145deg, color-mix(in srgb, var(--power-light) 30%, transparent), transparent 60%),
|
||||
radial-gradient(circle at 30% 35%, color-mix(in srgb, var(--power-accent) 26%, transparent), transparent 64%);
|
||||
}
|
||||
|
||||
.power-bg__aurora--right {
|
||||
right: -6%;
|
||||
bottom: -8%;
|
||||
width: 44%;
|
||||
height: 48%;
|
||||
background:
|
||||
linear-gradient(200deg, color-mix(in srgb, var(--power-light) 18%, transparent), transparent 62%),
|
||||
radial-gradient(circle at 52% 42%, color-mix(in srgb, var(--power-dark) 26%, transparent), transparent 68%);
|
||||
animation-duration: 22s;
|
||||
}
|
||||
|
||||
.power-bg__curtain {
|
||||
filter: blur(12px);
|
||||
opacity: 0.48;
|
||||
mix-blend-mode: screen;
|
||||
animation: powerCurtain 20s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.power-bg__curtain--left {
|
||||
left: -8%;
|
||||
top: 0;
|
||||
width: 34%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
color-mix(in srgb, var(--power-light) 24%, transparent) 0,
|
||||
color-mix(in srgb, var(--power-accent) 16%, transparent) 18%,
|
||||
transparent 68%
|
||||
);
|
||||
mask-image: linear-gradient(180deg, transparent 0, rgb(0 0 0 / 72%) 14%, rgb(0 0 0 / 78%) 88%, transparent 100%);
|
||||
}
|
||||
|
||||
.power-bg__curtain--right {
|
||||
right: -10%;
|
||||
top: 6%;
|
||||
width: 38%;
|
||||
height: 86%;
|
||||
background: linear-gradient(
|
||||
270deg,
|
||||
color-mix(in srgb, var(--power-light) 22%, transparent) 0,
|
||||
color-mix(in srgb, var(--power-dark) 14%, transparent) 20%,
|
||||
transparent 68%
|
||||
);
|
||||
mask-image: linear-gradient(180deg, transparent 0, rgb(0 0 0 / 58%) 18%, rgb(0 0 0 / 76%) 84%, transparent 100%);
|
||||
animation-duration: 24s;
|
||||
}
|
||||
|
||||
.power-bg__grid {
|
||||
opacity: 0.18;
|
||||
background-image:
|
||||
linear-gradient(to right, color-mix(in srgb, var(--power-deep) 10%, transparent) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, color-mix(in srgb, var(--power-deep) 8%, transparent) 1px, transparent 1px);
|
||||
background-size: 88px 88px;
|
||||
mask-image: radial-gradient(circle at center, rgb(0 0 0 / 68%) 0, rgb(0 0 0 / 18%) 58%, transparent 100%);
|
||||
}
|
||||
|
||||
.power-bg__scan {
|
||||
opacity: 0.2;
|
||||
background: linear-gradient(
|
||||
118deg,
|
||||
transparent 0 36%,
|
||||
color-mix(in srgb, var(--power-light) 7%, transparent) 43%,
|
||||
color-mix(in srgb, var(--power-light) 16%, transparent) 49%,
|
||||
color-mix(in srgb, var(--power-light) 7%, transparent) 56%,
|
||||
transparent 66%
|
||||
);
|
||||
transform: translateX(-18%);
|
||||
animation: powerSweep 18s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.power-bg__particles {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.power-bg__particle {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--power-light) 76%, white 24%);
|
||||
box-shadow: 0 0 22px color-mix(in srgb, var(--power-light) 28%, transparent);
|
||||
animation: powerParticle 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.power-bg__particle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -34px -2px;
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--power-light) 14%, transparent), transparent 68%);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.power-bg__particle--1 {
|
||||
top: 18%;
|
||||
left: 16%;
|
||||
animation-delay: -1s;
|
||||
}
|
||||
|
||||
.power-bg__particle--2 {
|
||||
top: 28%;
|
||||
right: 18%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
animation-delay: -4s;
|
||||
}
|
||||
|
||||
.power-bg__particle--3 {
|
||||
top: 58%;
|
||||
left: 24%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
animation-delay: -2s;
|
||||
}
|
||||
|
||||
.power-bg__particle--4 {
|
||||
top: 68%;
|
||||
right: 26%;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
animation-delay: -7s;
|
||||
}
|
||||
|
||||
.power-bg__particle--5 {
|
||||
top: 40%;
|
||||
left: 72%;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
animation-delay: -5s;
|
||||
}
|
||||
|
||||
.power-bg__particle--6 {
|
||||
top: 78%;
|
||||
left: 62%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
animation-delay: -8s;
|
||||
}
|
||||
|
||||
.power-bg__halo {
|
||||
border-radius: 999px;
|
||||
filter: blur(30px);
|
||||
opacity: 0.26;
|
||||
}
|
||||
|
||||
.power-bg__halo--top {
|
||||
top: 9%;
|
||||
left: 18%;
|
||||
width: 36%;
|
||||
height: 14%;
|
||||
background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--power-light) 24%, transparent), transparent);
|
||||
}
|
||||
|
||||
.power-bg__halo--bottom {
|
||||
right: 8%;
|
||||
bottom: 10%;
|
||||
width: 32%;
|
||||
height: 16%;
|
||||
background: radial-gradient(
|
||||
circle at center,
|
||||
color-mix(in srgb, var(--power-accent) 22%, transparent),
|
||||
transparent 72%
|
||||
);
|
||||
}
|
||||
|
||||
.power-bg__blob {
|
||||
animation: powerFloat 18s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.power-bg__blob--top {
|
||||
top: -900px;
|
||||
right: -300px;
|
||||
}
|
||||
|
||||
.power-bg__blob--bottom {
|
||||
bottom: -400px;
|
||||
left: -200px;
|
||||
animation-duration: 22s;
|
||||
animation-direction: alternate-reverse;
|
||||
}
|
||||
|
||||
.power-bg__panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.84;
|
||||
}
|
||||
|
||||
.power-bg__panel-arcs path,
|
||||
.power-bg__panel-dashes path,
|
||||
.power-bg__skyline path,
|
||||
.power-bg__panel-rings circle {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.power-bg__panel-arcs path {
|
||||
stroke: url(#power-arc-gradient);
|
||||
stroke-width: 2.2;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.power-bg__panel-dashes path {
|
||||
stroke: url(#power-dash-gradient);
|
||||
stroke-width: 1.6;
|
||||
stroke-dasharray: 10 12;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.power-bg__skyline path {
|
||||
fill: url(#power-skyline-gradient);
|
||||
stroke: color-mix(in srgb, var(--power-light) 24%, transparent);
|
||||
stroke-width: 1;
|
||||
opacity: 0.42;
|
||||
}
|
||||
|
||||
.power-bg__panel-rings circle {
|
||||
stroke: color-mix(in srgb, var(--power-light) 16%, transparent);
|
||||
stroke-width: 1;
|
||||
opacity: 0.18;
|
||||
animation: powerRing 15s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.power-bg__panel-rings circle:nth-child(2) {
|
||||
animation-delay: -4s;
|
||||
}
|
||||
|
||||
.power-bg__network {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.power-bg__lines path {
|
||||
fill: none;
|
||||
stroke: url(#power-line-gradient);
|
||||
stroke-width: 2;
|
||||
opacity: 0.84;
|
||||
}
|
||||
|
||||
.power-bg__flows path {
|
||||
fill: none;
|
||||
stroke: url(#power-flow-gradient);
|
||||
stroke-width: 3.2;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 22 246;
|
||||
animation: powerFlow 13s linear infinite;
|
||||
}
|
||||
|
||||
.power-bg__flows path:nth-child(2) {
|
||||
animation-duration: 15s;
|
||||
animation-delay: -4s;
|
||||
}
|
||||
|
||||
.power-bg__flows path:nth-child(3) {
|
||||
stroke-width: 2.6;
|
||||
animation-duration: 18s;
|
||||
animation-delay: -7s;
|
||||
}
|
||||
|
||||
.power-bg__nodes circle {
|
||||
fill: color-mix(in srgb, var(--power-light) 92%, white 8%);
|
||||
opacity: 0.78;
|
||||
animation: powerBlink 7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.power-bg__nodes circle:nth-child(2n) {
|
||||
animation-duration: 9s;
|
||||
}
|
||||
|
||||
.power-bg__nodes circle:nth-child(3n) {
|
||||
animation-delay: -2s;
|
||||
}
|
||||
|
||||
@keyframes powerFloat {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate3d(-18px, 14px, 0) scale(1.03);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(12px, -10px, 0) scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes powerPulse {
|
||||
0% {
|
||||
opacity: 0.9;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1.04);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes powerAurora {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.28;
|
||||
transform: translate3d(0, 0, 0) scale(0.98);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.48;
|
||||
transform: translate3d(18px, -12px, 0) scale(1.04);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes powerCurtain {
|
||||
0% {
|
||||
opacity: 0.32;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.52;
|
||||
transform: translate3d(18px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes powerSweep {
|
||||
0% {
|
||||
opacity: 0.06;
|
||||
transform: translateX(-18%);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.06;
|
||||
transform: translateX(18%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes powerParticle {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.48;
|
||||
transform: translate3d(0, 0, 0) scale(0.94);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.95;
|
||||
transform: translate3d(0, -10px, 0) scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes powerFlow {
|
||||
0% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dashoffset: -268;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes powerRing {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.08;
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.22;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes powerBlink {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.35;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.82;
|
||||
transform: scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.power-bg__blob--top {
|
||||
top: -1170px;
|
||||
right: -100px;
|
||||
}
|
||||
|
||||
.power-bg__blob--bottom {
|
||||
bottom: -760px;
|
||||
left: -100px;
|
||||
}
|
||||
|
||||
.power-bg__grid {
|
||||
background-size: 56px 56px;
|
||||
}
|
||||
|
||||
.power-bg__curtain {
|
||||
opacity: 0.34;
|
||||
}
|
||||
|
||||
.power-bg__particles {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.power-bg__network {
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.power-bg__panel {
|
||||
opacity: 0.52;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
61
src/components/custom/wave-bg.vue.bak
Normal file
61
src/components/custom/wave-bg.vue.bak
Normal 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>
|
||||
23
src/components/custom/web-site-link.vue
Normal file
23
src/components/custom/web-site-link.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'WebSiteLink' });
|
||||
|
||||
interface Props {
|
||||
/** Web site name */
|
||||
label: string;
|
||||
/** Web site link */
|
||||
link: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>
|
||||
<span>{{ label }}</span>
|
||||
<a class="text-blue-500" :href="link" target="#">
|
||||
{{ link }}
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user