moc变更管理系统框架
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
<script setup lang="ts" generic="T extends Record<string, unknown>, K = never">
|
||||
import { computed } from 'vue';
|
||||
import { VueDraggable } from 'vue-draggable-plus';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'TableColumnSetting'
|
||||
});
|
||||
|
||||
const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
|
||||
required: true
|
||||
});
|
||||
|
||||
const tooltipRecord: Record<NaiveUI.TableColumnFixed, App.I18n.I18nKey> = {
|
||||
left: 'datatable.fixed.right',
|
||||
right: 'datatable.fixed.unFixed',
|
||||
unFixed: 'datatable.fixed.left'
|
||||
};
|
||||
|
||||
function handleFixed(column: NaiveUI.TableColumnCheck) {
|
||||
const fixedOptions: NaiveUI.TableColumnFixed[] = ['left', 'right', 'unFixed'];
|
||||
const index = fixedOptions.findIndex(item => item === column.fixed);
|
||||
const nextIndex = index === fixedOptions.length - 1 ? 0 : index + 1;
|
||||
column.fixed = fixedOptions[nextIndex];
|
||||
}
|
||||
|
||||
const visibleStats = computed(() => {
|
||||
let total = 0;
|
||||
let checked = 0;
|
||||
|
||||
columns.value.forEach(column => {
|
||||
if (!column.visible) return;
|
||||
|
||||
total += 1;
|
||||
if (column.checked) checked += 1;
|
||||
});
|
||||
|
||||
return { total, checked };
|
||||
});
|
||||
|
||||
const selectAllChecked = computed(() => {
|
||||
const { total, checked } = visibleStats.value;
|
||||
|
||||
return total > 0 && checked === total;
|
||||
});
|
||||
|
||||
const selectAllIndeterminate = computed(() => {
|
||||
const { total, checked } = visibleStats.value;
|
||||
|
||||
return checked > 0 && checked < total;
|
||||
});
|
||||
|
||||
function toggleSelectAll(checked: boolean) {
|
||||
columns.value.forEach(column => {
|
||||
if (!column.visible) return;
|
||||
|
||||
column.checked = checked;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NPopover placement="bottom-end" trigger="click">
|
||||
<template #trigger>
|
||||
<NButton size="small">
|
||||
<template #icon>
|
||||
<icon-ant-design-setting-outlined class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.columnSetting') }}
|
||||
</NButton>
|
||||
</template>
|
||||
<div>
|
||||
<div class="h-36px flex-y-center rd-4px pl-26px hover:(bg-primary bg-opacity-20)">
|
||||
<NCheckbox
|
||||
:checked="selectAllChecked"
|
||||
:indeterminate="selectAllIndeterminate"
|
||||
:disabled="visibleStats.total === 0"
|
||||
class="flex-1"
|
||||
@update:checked="toggleSelectAll"
|
||||
>
|
||||
{{ $t('common.selectAll') }}
|
||||
</NCheckbox>
|
||||
</div>
|
||||
<NDivider class="!my-4px" />
|
||||
<VueDraggable
|
||||
v-model="columns"
|
||||
:animation="150"
|
||||
filter=".none_draggable"
|
||||
class="column-setting-scroll max-h-[200px] overflow-y-auto"
|
||||
>
|
||||
<div
|
||||
v-for="item in columns"
|
||||
:key="item.key"
|
||||
class="h-36px flex-y-center justify-between gap-6px"
|
||||
:class="{ hidden: !item.visible }"
|
||||
>
|
||||
<div class="h-full flex-y-center flex-1 rd-4px hover:(bg-primary bg-opacity-20)">
|
||||
<icon-mdi-drag class="mr-8px h-full cursor-move text-icon" />
|
||||
<NCheckbox v-model:checked="item.checked" class="none_draggable flex-1">
|
||||
<template v-if="typeof item.title === 'function'">
|
||||
<component :is="item.title" />
|
||||
</template>
|
||||
<template v-else>{{ item.title }}</template>
|
||||
</NCheckbox>
|
||||
</div>
|
||||
<ButtonIcon
|
||||
:disabled="!item.checked"
|
||||
:focusable="false"
|
||||
:tooltip-content="$t(tooltipRecord[item.fixed!])"
|
||||
@click="handleFixed(item)"
|
||||
>
|
||||
<icon-octicon-pin-16 v-if="item.fixed === 'unFixed'" />
|
||||
<icon-octicon-pin-16 v-else-if="item.fixed === 'left'" class="rotate-270" />
|
||||
<icon-octicon-pin-slash-16 v-else />
|
||||
</ButtonIcon>
|
||||
</div>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
</NPopover>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.column-setting-scroll {
|
||||
@include scrollbar();
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'TableHeaderOperation'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
itemAlign?: NaiveUI.Align;
|
||||
disabledDelete?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'add'): void;
|
||||
(e: 'delete'): void;
|
||||
(e: 'refresh'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
|
||||
default: () => []
|
||||
});
|
||||
|
||||
function add() {
|
||||
emit('add');
|
||||
}
|
||||
|
||||
function batchDelete() {
|
||||
emit('delete');
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
emit('refresh');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace :align="itemAlign" wrap justify="end" class="lt-sm:w-200px">
|
||||
<slot name="prefix"></slot>
|
||||
<slot name="default">
|
||||
<NButton size="small" ghost type="primary" @click="add">
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.add') }}
|
||||
</NButton>
|
||||
<NPopconfirm @positive-click="batchDelete">
|
||||
<template #trigger>
|
||||
<NButton size="small" ghost type="error" :disabled="disabledDelete">
|
||||
<template #icon>
|
||||
<icon-ic-round-delete class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.batchDelete') }}
|
||||
</NButton>
|
||||
</template>
|
||||
{{ $t('common.confirmDelete') }}
|
||||
</NPopconfirm>
|
||||
</slot>
|
||||
<NButton size="small" @click="refresh">
|
||||
<template #icon>
|
||||
<icon-mdi-refresh class="text-icon" :class="{ 'animate-spin': loading }" />
|
||||
</template>
|
||||
{{ $t('common.refresh') }}
|
||||
</NButton>
|
||||
<TableColumnSetting v-model:columns="columns" />
|
||||
<slot name="suffix"></slot>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { createTextVNode, defineComponent } 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();
|
||||
|
||||
return () => createTextVNode();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NLoadingBarProvider>
|
||||
<NDialogProvider>
|
||||
<NNotificationProvider>
|
||||
<NMessageProvider>
|
||||
<ContextHolder />
|
||||
<slot></slot>
|
||||
</NMessageProvider>
|
||||
</NNotificationProvider>
|
||||
</NDialogProvider>
|
||||
</NLoadingBarProvider>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'DarkModeContainer' });
|
||||
|
||||
interface Props {
|
||||
inverted?: boolean;
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'ExceptionBase' });
|
||||
|
||||
type ExceptionType = '403' | '404' | '500';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Exception type
|
||||
*
|
||||
* - 403: no permission
|
||||
* - 404: not found
|
||||
* - 500: service error
|
||||
*/
|
||||
type: ExceptionType;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const iconMap: Record<ExceptionType, string> = {
|
||||
'403': 'no-permission',
|
||||
'404': 'not-found',
|
||||
'500': 'service-error'
|
||||
};
|
||||
|
||||
const icon = computed(() => iconMap[props.type]);
|
||||
</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="icon" />
|
||||
</div>
|
||||
<NButton type="primary" @click="routerPushByKey('root')">{{ $t('common.backToHome') }}</NButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, useSlots } from 'vue';
|
||||
import type { PopoverPlacement } from 'naive-ui';
|
||||
|
||||
defineOptions({ name: 'IconTooltip' });
|
||||
|
||||
interface Props {
|
||||
icon?: string;
|
||||
localIcon?: string;
|
||||
desc?: string;
|
||||
placement?: PopoverPlacement;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
icon: 'mdi-help-circle',
|
||||
localIcon: '',
|
||||
desc: '',
|
||||
placement: 'top'
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
const hasCustomTrigger = computed(() => Boolean(slots.trigger));
|
||||
|
||||
if (!hasCustomTrigger.value && !props.icon && !props.localIcon) {
|
||||
throw new Error('icon or localIcon is required when no custom trigger slot is provided');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NTooltip :placement="placement">
|
||||
<template #trigger>
|
||||
<slot name="trigger">
|
||||
<div class="cursor-pointer">
|
||||
<SvgIcon :icon="icon" :local-icon="localIcon" />
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
<slot>
|
||||
<span>{{ desc }}</span>
|
||||
</slot>
|
||||
</NTooltip>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<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 emit = defineEmits<Emits>();
|
||||
|
||||
const tooltipContent = computed(() => {
|
||||
if (!props.showTooltip) return '';
|
||||
|
||||
return $t('icon.lang');
|
||||
});
|
||||
|
||||
/** Add bottom margin to all options except the last one for proper visual separation */
|
||||
const dropdownOptions = computed(() => {
|
||||
const lastIndex = props.langOptions.length - 1;
|
||||
|
||||
return props.langOptions.map((option, index) => ({
|
||||
...option,
|
||||
props: {
|
||||
class: index < lastIndex ? 'mb-1' : undefined
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
function changeLang(lang: App.I18n.LangType) {
|
||||
emit('changeLang', lang);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDropdown :value="lang" :options="dropdownOptions" trigger="hover" @select="changeLang">
|
||||
<div>
|
||||
<ButtonIcon :tooltip-content="tooltipContent" tooltip-placement="left">
|
||||
<SvgIcon icon="heroicons:language" />
|
||||
</ButtonIcon>
|
||||
</div>
|
||||
</NDropdown>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'MenuToggler' });
|
||||
|
||||
interface Props {
|
||||
/** Show collapsed icon */
|
||||
collapsed?: boolean;
|
||||
/** Arrow style icon */
|
||||
arrowIcon?: boolean;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
arrowIcon: false,
|
||||
zIndex: 98
|
||||
});
|
||||
|
||||
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
|
||||
:key="String(collapsed)"
|
||||
:tooltip-content="collapsed ? $t('icon.expand') : $t('icon.collapse')"
|
||||
tooltip-placement="bottom-start"
|
||||
:z-index="zIndex"
|
||||
>
|
||||
<SvgIcon :icon="icon" />
|
||||
</ButtonIcon>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
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.unpin') : $t('icon.pin')"
|
||||
tooltip-placement="bottom-start"
|
||||
:z-index="100"
|
||||
>
|
||||
<SvgIcon :icon="icon" />
|
||||
</ButtonIcon>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="app-logo">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
version="1.1"
|
||||
viewBox="0 0 1000 1000"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M 200,866 C 100,866 50,779.4 100,692.8 L 200,519.6 C 220,485 240,490 265,499.6 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
|
||||
fill="url(#LinearGradient)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M 420,866 C 455,861 478,846 500,827 C 614,696 615,597 500,517 C 394,444 333,374 380,207.82 L 260,415.67 C 240.22,450 254.37,465.1 275.28,481.79 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
|
||||
fill="url(#LinearGradient_2)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M 500,517 C 394,444 333,374 380,207.82 L 400,173.2 C 367,295 421,350 603,428 C 572,440 524,474 500,517"
|
||||
fill="url(#LinearGradient_3)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 572,440 524,474 500,517 C 615,597 614,696 500,827"
|
||||
fill="url(#LinearGradient_4)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M 400,173.2 C 367,295 421,350 603,428 C 690,389, 750,445 788,500 L 600,173.2 C 550,86.6 450,86.6 400,173.2"
|
||||
fill="url(#LinearGradient_5)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 690,389, 750,445 788,500 C 816,554 797,606 750,640 L 500,827"
|
||||
fill="url(#LinearGradient_6)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M 788,500 C 816,554 797,606 750,640 L 500,827 C 497,851 513,862 540,866 L 800,866 C 900,866 950,779.4 900,692.8 L 788,500"
|
||||
fill="url(#LinearGradient_7)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="LinearGradient"
|
||||
gradientTransform="matrix(104.391 -73.3432 73.3432 104.391 277.441 710.122)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-700)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-600)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="LinearGradient_2"
|
||||
gradientTransform="matrix(-173.747 557.324 -557.324 -173.747 508.829 258.172)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-300)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-500)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="LinearGradient_3"
|
||||
gradientTransform="matrix(157.951 295.666 -295.666 157.951 382.944 193.642)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-600)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-700)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="LinearGradient_4"
|
||||
gradientTransform="matrix(-44.3023 219.578 -219.578 -44.3023 619.69 469.652)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-400)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-600)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="LinearGradient_5"
|
||||
gradientTransform="matrix(125.52 334.256 -334.256 125.52 539.723 235.139)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-500)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-300)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="LinearGradient_6"
|
||||
gradientTransform="matrix(-241.23 357.206 -357.206 -241.23 754.054 449.312)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-300)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-500)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="LinearGradient_7"
|
||||
gradientTransform="matrix(125.978 210.065 -210.065 125.978 596.433 613.665)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-700)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-600)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-logo {
|
||||
--logo-color-300: rgb(var(--primary-300-color));
|
||||
--logo-color-400: rgb(var(--primary-400-color));
|
||||
--logo-color-500: rgb(var(--primary-500-color));
|
||||
--logo-color-600: rgb(var(--primary-600-color));
|
||||
--logo-color-700: rgb(var(--primary-700-color));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverPlacement } from 'naive-ui';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
defineOptions({
|
||||
name: 'ButtonIcon',
|
||||
inheritAttrs: false
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** Button class */
|
||||
class?: string;
|
||||
/** Iconify icon name */
|
||||
icon?: string;
|
||||
/** Tooltip content */
|
||||
tooltipContent?: string;
|
||||
/** Tooltip placement */
|
||||
tooltipPlacement?: PopoverPlacement;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
class: '',
|
||||
icon: '',
|
||||
tooltipContent: '',
|
||||
tooltipPlacement: 'bottom',
|
||||
zIndex: 98
|
||||
});
|
||||
|
||||
const DEFAULT_CLASS = 'h-[36px] text-icon';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NTooltip :placement="tooltipPlacement" :z-index="zIndex" :disabled="!tooltipContent">
|
||||
<template #trigger>
|
||||
<NButton quaternary :class="twMerge(DEFAULT_CLASS, props.class)" v-bind="$attrs">
|
||||
<div class="flex-center gap-8px">
|
||||
<slot>
|
||||
<SvgIcon :icon="icon" />
|
||||
</slot>
|
||||
</div>
|
||||
</NButton>
|
||||
</template>
|
||||
{{ tooltipContent }}
|
||||
</NTooltip>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
defineOptions({ name: 'CustomIconSelect' });
|
||||
|
||||
interface Props {
|
||||
/** Selected icon */
|
||||
value: string;
|
||||
/** List of icons */
|
||||
icons: string[];
|
||||
/** Icon for when nothing is selected */
|
||||
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>
|
||||
|
||||
<template>
|
||||
<NPopover placement="bottom-end" trigger="click">
|
||||
<template #trigger>
|
||||
<NInput v-model:value="modelValue" readonly placeholder="点击选择图标">
|
||||
<template #suffix>
|
||||
<SvgIcon :icon="selectedIcon" class="p-5px text-30px" />
|
||||
</template>
|
||||
</NInput>
|
||||
</template>
|
||||
<template #header>
|
||||
<NInput v-model:value="searchValue" placeholder="搜索图标"></NInput>
|
||||
</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)">
|
||||
<SvgIcon
|
||||
:icon="iconItem"
|
||||
class="m-2px cursor-pointer border-1px border-#d9d9d9 p-5px text-30px"
|
||||
:class="{ 'border-primary': modelValue === iconItem }"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<NEmpty v-else class="w-306px" description="你什么也找不到" />
|
||||
</NPopover>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.n-input-wrapper) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
:deep(.n-input__suffix) {
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'LookForward'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="size-full min-h-520px flex-col-center gap-24px overflow-hidden">
|
||||
<div class="flex text-400px text-primary">
|
||||
<SvgIcon local-icon="expectation" />
|
||||
</div>
|
||||
<slot>
|
||||
<h3 class="text-28px text-primary font-500">{{ $t('common.lookForward') }}</h3>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'SoybeanAvatar'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="size-72px overflow-hidden rd-1/2">
|
||||
<img src="@/assets/imgs/soybean.jpg" class="size-full" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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