init
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
|
||||
export type AvatarType = {
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
defineProps({
|
||||
source: Array as PropType<AvatarType[]>,
|
||||
size: {
|
||||
type: Number,
|
||||
default: 22,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="avatar-list">
|
||||
<a-avatar :style="`margin-left: -${size / 2}px`" v-for="(item, i) in source" :size="size" :key="i"
|
||||
:src="item.avatar" />
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.avatar-list {
|
||||
:deep(.ant-avatar) {
|
||||
@apply outline-1 outline-white outline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div style="height: 240px" ref="container" class="bar-chart"></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, PropType, ref } from 'vue';
|
||||
import { EChartsType, Color } from 'echarts';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const container = ref<HTMLElement>();
|
||||
|
||||
let chart: EChartsType | null = null;
|
||||
|
||||
const props = defineProps({
|
||||
color: Array as PropType<Color[]>,
|
||||
list: Array,
|
||||
});
|
||||
|
||||
function resize() {
|
||||
chart?.resize();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
chart = echarts.init(container.value!);
|
||||
chart.setOption({
|
||||
color: props.color ?? ['#ff0000'],
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 1,
|
||||
y2: 0,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: '#00369e',
|
||||
},
|
||||
{
|
||||
offset: 0.33,
|
||||
color: '#005cfd',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#a18dff',
|
||||
},
|
||||
],
|
||||
},
|
||||
grid: [
|
||||
{
|
||||
top: 40,
|
||||
left: 56,
|
||||
right: 20,
|
||||
bottom: 40,
|
||||
},
|
||||
],
|
||||
xAxis: [
|
||||
{
|
||||
name: '时间',
|
||||
nameTextStyle: { color: 'rgba(0, 0, 0, 0)' },
|
||||
type: 'category',
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
darkMode: true,
|
||||
yAxis: [
|
||||
{
|
||||
name: '销售额',
|
||||
nameTextStyle: { color: 'rgba(0, 0, 0, 0)' },
|
||||
type: 'value',
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
width: 2,
|
||||
color: 'rgba(255, 255, 255, 0.25)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: 24,
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
data: props.list,
|
||||
},
|
||||
],
|
||||
});
|
||||
window.addEventListener('resize', resize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resize);
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.bar-chart {
|
||||
:deep(canvas) {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div
|
||||
style="width: 100%; height: 400px"
|
||||
class="line-chart"
|
||||
ref="container"
|
||||
></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue';
|
||||
import type { EChartsType } from 'echarts';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
let chart: EChartsType | null = null;
|
||||
const container = ref<HTMLElement>();
|
||||
|
||||
function resize() {
|
||||
chart?.resize();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
chart = echarts.init(container.value!);
|
||||
chart.setOption({
|
||||
color: ['#005af9', '#985af9'],
|
||||
grid: [
|
||||
{
|
||||
top: 100,
|
||||
left: 32,
|
||||
right: 12,
|
||||
bottom: 20,
|
||||
},
|
||||
],
|
||||
xAxis: [
|
||||
{
|
||||
name: '时间',
|
||||
nameTextStyle: { color: 'rgba(0 , 0, 0, 0)' },
|
||||
type: 'category',
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: false },
|
||||
boundaryGap: 0,
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
name: '销售额',
|
||||
nameTextStyle: { color: 'rgba(0 , 0, 0, 0)' },
|
||||
type: 'value',
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: false },
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
width: 2,
|
||||
color: 'rgba(0, 0, 0, 0.15)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
legend: {
|
||||
show: true,
|
||||
right: '8',
|
||||
top: 0,
|
||||
orient: 'vertical',
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '销售额',
|
||||
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
data: [
|
||||
['一月', 12],
|
||||
['二月', 8],
|
||||
['三月', 92],
|
||||
['四月', 32],
|
||||
['五月', 22],
|
||||
['六月', 89],
|
||||
['七月', 72],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '订单',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
width: 4,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
data: [
|
||||
['一月', 12],
|
||||
['二月', 8],
|
||||
['三月', 24],
|
||||
['四月', 32],
|
||||
['五月', 56],
|
||||
['六月', 56],
|
||||
['七月', 56],
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
window.addEventListener('resize', resize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resize);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, PropType, ref, toRefs } from 'vue';
|
||||
|
||||
export type AntInputType =
|
||||
| 'input'
|
||||
| 'textarea'
|
||||
| 'radio'
|
||||
| 'timePicker'
|
||||
| 'datePicker'
|
||||
| 'rangePicker'
|
||||
| 'select'
|
||||
| 'mention'
|
||||
| 'rate'
|
||||
| 'upload'
|
||||
| 'treeSelect'
|
||||
| 'transfer'
|
||||
| 'checkbox'
|
||||
| 'cascader'
|
||||
| 'autoComplete'
|
||||
| 'inputNumber'
|
||||
| 'slider'
|
||||
| 'switch';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:edit', edit: boolean): void;
|
||||
(e: 'update:value', value: any): void;
|
||||
(e: 'pressEnter', event: KeyboardEvent): void;
|
||||
}>();
|
||||
const props = defineProps({
|
||||
value: [String, Number, Boolean, Object],
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
editOnClick: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<AntInputType>,
|
||||
default: 'input',
|
||||
validator(val: string) {
|
||||
return [
|
||||
'input',
|
||||
'radio',
|
||||
'timePicker',
|
||||
'datePicker',
|
||||
'rangePicker',
|
||||
'select',
|
||||
'mention',
|
||||
'rate',
|
||||
'upload',
|
||||
'treeSelect',
|
||||
'transfer',
|
||||
'checkbox',
|
||||
'cascader',
|
||||
'autoComplete',
|
||||
'inputNumber',
|
||||
'slider',
|
||||
'switch',
|
||||
'textarea',
|
||||
].includes(val);
|
||||
},
|
||||
},
|
||||
options: Object,
|
||||
});
|
||||
|
||||
const { type } = toRefs(props);
|
||||
|
||||
const component = computed(() => {
|
||||
const _type = type.value;
|
||||
return 'A' + _type.substring(0, 1).toUpperCase() + _type.substring(1);
|
||||
});
|
||||
|
||||
const cacheEdit = ref(false);
|
||||
|
||||
const _edit = computed({
|
||||
get() {
|
||||
if (props.edit !== null) {
|
||||
cacheEdit.value = props.edit;
|
||||
}
|
||||
return props.edit ?? cacheEdit.value;
|
||||
},
|
||||
set(val) {
|
||||
cacheEdit.value = val;
|
||||
emit('update:edit', val);
|
||||
},
|
||||
});
|
||||
|
||||
const input = ref();
|
||||
|
||||
function editCell() {
|
||||
if (props.editOnClick) {
|
||||
_edit.value = true;
|
||||
nextTick(() => input.value?.focus());
|
||||
}
|
||||
}
|
||||
|
||||
function complete() {
|
||||
_edit.value = false;
|
||||
}
|
||||
|
||||
const cacheVal: any = ref(null);
|
||||
const _value = computed({
|
||||
get() {
|
||||
if (props.value !== undefined) {
|
||||
cacheVal.value = props.value;
|
||||
}
|
||||
return props.value ?? cacheVal.value;
|
||||
},
|
||||
set(val) {
|
||||
cacheVal.value = val;
|
||||
emit('update:value', val);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<slot v-if="_edit" class="editable-cell-input" name="input">
|
||||
<component
|
||||
ref="input"
|
||||
@keyup.enter="complete"
|
||||
@blur="complete"
|
||||
v-model:value="_value"
|
||||
class="editable-cell-input-component"
|
||||
v-bind="options"
|
||||
:is="component"
|
||||
/>
|
||||
</slot>
|
||||
<div v-else @click="editCell" class="editable-cell-show">
|
||||
<slot>
|
||||
{{ value }}
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.editable-cell {
|
||||
&-input {
|
||||
&-component {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import EditableCell from './EditableCell.vue';
|
||||
export default EditableCell;
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div v-if="parts" class="splitter">
|
||||
<template v-for="(part, i) in parts" :key="i">
|
||||
<span class="splitter-part">{{ part }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Splitter',
|
||||
props: {
|
||||
value: String,
|
||||
partLength: { type: Number, default: 4 },
|
||||
sensitive: Array as PropType<number[]>,
|
||||
},
|
||||
setup(props, { attrs, slots, emit }) {},
|
||||
computed: {
|
||||
parts(): string[] {
|
||||
let { value = '', partLength, sensitive = [] } = this;
|
||||
const [start = -1, end = -1] = sensitive;
|
||||
let sense = '';
|
||||
for (let i = 0; i < end - start; i++) {
|
||||
sense += '*';
|
||||
}
|
||||
value = `${value.substring(0, start)}${sense}${value.substring(
|
||||
end,
|
||||
value.length
|
||||
)}`;
|
||||
|
||||
const parts = [];
|
||||
for (let i = 0; i < value.length; i += partLength) {
|
||||
parts.push(value.substring(i, i + partLength));
|
||||
}
|
||||
return parts;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.splitter {
|
||||
&-part {
|
||||
&:not(:first-child) {
|
||||
@apply ml-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, PropType, watch } from 'vue';
|
||||
import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons-vue';
|
||||
import { useFullScreen } from '@/utils/htmlHelper';
|
||||
|
||||
const prop = defineProps({
|
||||
target: { type: [String, Object] as PropType<string | HTMLElement>, required: true },
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', value: boolean);
|
||||
}>();
|
||||
|
||||
const { enterFullScreen, exitFullscreen, isEnter } = useFullScreen(prop.target);
|
||||
|
||||
function toggle() {
|
||||
if (isEnter.value) {
|
||||
exitFullscreen();
|
||||
} else {
|
||||
enterFullScreen();
|
||||
}
|
||||
}
|
||||
|
||||
watch(isEnter, (val) => emit('change', val));
|
||||
</script>
|
||||
<template>
|
||||
<div class="inline-block text-lg" @click="toggle">
|
||||
<FullscreenExitOutlined v-if="isEnter" />
|
||||
<FullscreenOutlined v-else />
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="less"></style>
|
||||
@@ -0,0 +1,366 @@
|
||||
<script lang="ts" setup>
|
||||
import { offsetScreen } from '@/utils/htmlHelper';
|
||||
import { GuiderOption, GuideTarget } from './interface';
|
||||
import { PropType, watch, reactive, computed, ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
current: [HTMLElement, Object, String] as PropType<GuideTarget>,
|
||||
options: {
|
||||
required: true,
|
||||
type: Array<GuiderOption>,
|
||||
},
|
||||
show: Boolean,
|
||||
});
|
||||
|
||||
const index = ref(0);
|
||||
|
||||
const currentIndex = computed({
|
||||
get(): number {
|
||||
if (!props.current) {
|
||||
return index.value;
|
||||
}
|
||||
index.value = props.options.findIndex((item) => item.target === props.current);
|
||||
return index.value;
|
||||
},
|
||||
set(val) {
|
||||
const target = props.options[val];
|
||||
if (target) {
|
||||
index.value = val;
|
||||
emit('update:current', target.target);
|
||||
} else {
|
||||
index.value = 0;
|
||||
emit('close');
|
||||
emit('update:current', props.options[0].target);
|
||||
emit('update:show', false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const doc = ref<HTMLElement>();
|
||||
|
||||
const flag = ref(props.show);
|
||||
|
||||
const location = reactive({
|
||||
width: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
});
|
||||
|
||||
// 事件
|
||||
// defineEmits(['close', 'update:show', 'update:current'])
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'update:show', show: boolean): void;
|
||||
(e: 'update:current', target?: GuideTarget): void;
|
||||
}>();
|
||||
|
||||
// 目标 html 元素
|
||||
const targetEl = computed<HTMLElement>(() => {
|
||||
const _el = props.current ?? props.options[index.value]?.target;
|
||||
if (!_el) {
|
||||
return;
|
||||
}
|
||||
if (typeof _el === 'string') {
|
||||
return document.querySelector(_el);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
return _el instanceof HTMLElement ? _el : _el.$el;
|
||||
}
|
||||
});
|
||||
|
||||
// 方位
|
||||
type Direction = 'vertical' | 'horizontal';
|
||||
// 朝向
|
||||
type Site = 'top' | 'right' | 'bottom' | 'left';
|
||||
// 位置
|
||||
type Placement = {
|
||||
main: Direction;
|
||||
sub: Direction;
|
||||
vertical: Site | 'center';
|
||||
horizontal: Site | 'center';
|
||||
};
|
||||
|
||||
// doc 文档显示位置
|
||||
const placement = computed<Placement>(() => {
|
||||
const { offsetWidth: docWidth, offsetHeight: docHeight } = doc.value || {
|
||||
offsetWidth: 0,
|
||||
offsetHeight: 0,
|
||||
};
|
||||
const { left: tLeft, top: tTop, right: tRight, bottom: tBottom, height: tHeight, width: tWidth } = location;
|
||||
|
||||
const p: Placement = {
|
||||
main: 'horizontal',
|
||||
sub: 'vertical',
|
||||
horizontal: 'left',
|
||||
vertical: 'top',
|
||||
};
|
||||
|
||||
// 判断文档位置在主方向为水平还是垂直
|
||||
if (Math.max(tLeft, tRight) >= Math.max(tTop, tBottom)) {
|
||||
p.main = 'horizontal';
|
||||
p.sub = 'vertical';
|
||||
p.horizontal = tLeft > tRight ? 'left' : 'right';
|
||||
if (tTop + tHeight / 2 < docHeight / 2 && tBottom + tHeight / 2 > docHeight / 2) {
|
||||
p.vertical = 'top';
|
||||
} else if (tTop + tHeight / 2 > docHeight / 2 && tBottom + tHeight / 2 < docHeight / 2) {
|
||||
p.vertical = 'bottom';
|
||||
} else {
|
||||
p.vertical = 'center';
|
||||
}
|
||||
} else {
|
||||
p.main = 'vertical';
|
||||
p.sub = 'horizontal';
|
||||
p.vertical = tTop > tBottom ? 'top' : 'bottom';
|
||||
if (tLeft + tWidth / 2 < docWidth / 2 && tRight + tWidth / 2 > docWidth / 2) {
|
||||
p.horizontal = 'left';
|
||||
} else if (tLeft + tWidth / 2 > docWidth / 2 && tRight + tWidth / 2 < docWidth / 2) {
|
||||
p.horizontal = 'right';
|
||||
} else {
|
||||
p.horizontal = 'center';
|
||||
}
|
||||
}
|
||||
return p;
|
||||
});
|
||||
|
||||
type Position = {
|
||||
top?: number;
|
||||
right?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
};
|
||||
|
||||
// 指引文档位置
|
||||
const docPosition = computed(() => {
|
||||
const p: Position = { left: 0, top: 0 };
|
||||
if (!props.show || !doc.value || !flag.value) {
|
||||
return p;
|
||||
}
|
||||
|
||||
const { top, left, right, bottom, height, width } = location;
|
||||
|
||||
const place = placement.value;
|
||||
const main = place[place.main] as Site;
|
||||
const sub = place[place.sub];
|
||||
|
||||
const margin = 10;
|
||||
const offset = (place.main === 'horizontal' ? doc.value?.offsetWidth : doc.value?.offsetHeight) ?? 0;
|
||||
p[main] = location[main] - offset - margin;
|
||||
|
||||
if (main === 'right') {
|
||||
p.left = left + width + margin;
|
||||
} else if (main === 'bottom') {
|
||||
p.top = top + height + margin;
|
||||
}
|
||||
|
||||
if (sub === 'center') {
|
||||
if (place.main === 'horizontal') {
|
||||
p.top = top + height / 2 - (doc.value?.offsetHeight ?? 0) / 2;
|
||||
} else {
|
||||
p.left = left + width / 2 - (doc.value?.offsetWidth ?? 0) / 2;
|
||||
}
|
||||
} else {
|
||||
p[sub] = location[sub];
|
||||
}
|
||||
|
||||
if (p.left === undefined) {
|
||||
p.left = window.innerWidth - p.right! - doc.value?.offsetWidth!;
|
||||
}
|
||||
|
||||
if (p.top === undefined) {
|
||||
p.top = window.innerHeight - p.bottom! - doc.value?.offsetHeight!;
|
||||
}
|
||||
|
||||
return p;
|
||||
});
|
||||
|
||||
// 指示箭头位置
|
||||
const arrowStyle = computed(() => {
|
||||
const p: Position = { left: -16, top: -16 };
|
||||
|
||||
if (!props.show || !doc.value || !flag.value) {
|
||||
return p;
|
||||
}
|
||||
|
||||
const place = placement.value;
|
||||
const main = place[place.main] as Site;
|
||||
const sub = place[place.sub];
|
||||
|
||||
const { offsetHeight = 0, offsetWidth = 0 } = doc.value ?? {};
|
||||
|
||||
if (main === 'left') {
|
||||
p.left = (offsetWidth ?? 0) - 4;
|
||||
} else if (main === 'top') {
|
||||
p.top = (offsetHeight ?? 0) - 4;
|
||||
}
|
||||
|
||||
if (place.main === 'horizontal') {
|
||||
if (sub === 'center') {
|
||||
p.top = offsetHeight / 2 - 10;
|
||||
} else if (sub === 'bottom') {
|
||||
p.top = offsetHeight - location.height / 2 - 10;
|
||||
} else {
|
||||
p.top = location.height / 2 - 10;
|
||||
}
|
||||
}
|
||||
|
||||
if (place.main === 'vertical') {
|
||||
if (sub === 'center') {
|
||||
p.left = offsetWidth / 2 - 10;
|
||||
} else if (sub === 'right') {
|
||||
p.left = offsetWidth - location.width / 2 - 10;
|
||||
} else {
|
||||
p.left = location.width / 2 - 10;
|
||||
}
|
||||
}
|
||||
return p;
|
||||
});
|
||||
|
||||
/**
|
||||
* 设置目标元素位置
|
||||
*/
|
||||
function setPosition() {
|
||||
const el = targetEl.value;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
const p = offsetScreen(el);
|
||||
location.left = p[0];
|
||||
location.top = p[1];
|
||||
location.width = el.offsetWidth;
|
||||
location.height = el.offsetHeight;
|
||||
location.right = window.innerWidth - location.width - location.left;
|
||||
location.bottom = window.innerHeight - location.height - location.top;
|
||||
}
|
||||
|
||||
watch(() => [targetEl.value, props.show], setPosition);
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(val) => {
|
||||
flag.value = val;
|
||||
},
|
||||
{
|
||||
flush: 'post',
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', setPosition);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', setPosition);
|
||||
});
|
||||
|
||||
function nextStep() {
|
||||
currentIndex.value += 1;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
emit('close');
|
||||
emit('update:show', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="guider" :style="`display: ${show ? 'static' : 'none'}`">
|
||||
<div class="guider-left" :style="`border-left: ${location.left - 2}px solid rgba(0, 0, 0, 0.25);`"></div>
|
||||
<div
|
||||
class="guider-top"
|
||||
:style="`left: ${location.left - 2}px; width: ${location.width + 4}px; border-top: ${
|
||||
location.top - 2
|
||||
}px solid rgba(0, 0, 0, 0.25);`"
|
||||
></div>
|
||||
<div
|
||||
class="guider-right"
|
||||
:style="`left: ${location.left + location.width + 2}px; background-color: rgba(0, 0, 0, 0.25)`"
|
||||
></div>
|
||||
<div
|
||||
class="guider-bottom"
|
||||
:style="`left: ${location.left - 2}px; width: ${location.width + 4}px; top: ${
|
||||
location.top + location.height + 2
|
||||
}px`"
|
||||
></div>
|
||||
<div
|
||||
ref="doc"
|
||||
class="guider-doc flex flex-col justify-between rounded-md"
|
||||
:style="`left: ${docPosition.left}px;top:${docPosition.top}px`"
|
||||
>
|
||||
<div
|
||||
class="arrow"
|
||||
:style="`left: ${arrowStyle.left}px; top: ${arrowStyle.top}px; border-${
|
||||
placement[placement.main]
|
||||
}-color: white`"
|
||||
></div>
|
||||
<div class="guider-content">
|
||||
<h1>第一步</h1>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guider-footer flex justify-between w-full">
|
||||
<a-button @click="onClose">关闭</a-button>
|
||||
<a-button type="primary" @click="nextStep">下一步</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.guider-left {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
height: 100vh;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.guider-top {
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.guider-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.guider-bottom {
|
||||
position: fixed;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.guider-doc {
|
||||
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.125);
|
||||
width: 20%;
|
||||
height: 200px;
|
||||
background-color: white;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.5);
|
||||
padding: 6px 8px;
|
||||
|
||||
.arrow {
|
||||
transition: all 0.25s ease-in;
|
||||
border-width: 10px;
|
||||
z-index: 9;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.guider-footer {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
import Guider from './Guider.vue';
|
||||
export type { GuideTarget, GuiderOption } from './interface';
|
||||
|
||||
export default Guider;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { ComponentPublicInstance, FunctionalComponent, AsyncComponentOptions, AsyncComponentLoader } from 'vue';
|
||||
|
||||
export type GuideTarget = HTMLElement | ComponentPublicInstance | String;
|
||||
export type GuiderOption = {
|
||||
target?: GuideTarget;
|
||||
doc?: GuideTarget | FunctionalComponent | AsyncComponentOptions | AsyncComponentLoader;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
<script lang="ts" setup></script>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="common-view">
|
||||
<div class="common-header">
|
||||
<div class="common-header-main">
|
||||
<div class="logo">
|
||||
<img class="img" src="@/assets/vite.svg" />
|
||||
</div>
|
||||
<div class="navigation">
|
||||
<div class="nav-item">
|
||||
<stepin-link to="/login"> 文档 </stepin-link>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<stepin-link to="/login"> API </stepin-link>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<stepin-link to="/login"> 关于 </stepin-link>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<stepin-link to="/login"> 商业合作 </stepin-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a-button class="login-btn" type="primary">注册</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="common-content">
|
||||
<div class="main">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup></script>
|
||||
<style scoped lang="less">
|
||||
.common-view {
|
||||
display: grid;
|
||||
min-height: 100vh;
|
||||
grid-template-rows: 64px 1fr;
|
||||
@apply bg-gray-800;
|
||||
.common-header {
|
||||
@apply bg-gray-800 flex items-center pt-lg;
|
||||
&-main {
|
||||
width: 1400px;
|
||||
margin: 0 auto;
|
||||
@apply flex items-center;
|
||||
.logo {
|
||||
flex: none;
|
||||
.img {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
.navigation {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.nav-item {
|
||||
font-size: 18px;
|
||||
margin-left: 64px;
|
||||
.stepin-link {
|
||||
color: theme('colors.text-inverse');
|
||||
&:hover {
|
||||
color: theme('colors.primary.500');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.actions {
|
||||
flex: none;
|
||||
.login-btn {
|
||||
height: 38px;
|
||||
width: 88px;
|
||||
font-size: 16px;
|
||||
@apply bg-gray-700 border-gray-600 rounded-md hover:bg-gray-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.common-content {
|
||||
.main {
|
||||
width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<script lang="ts" setup>
|
||||
import { LogoutOutlined } from '@ant-design/icons-vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { ThemeProvider, alert } from 'stepin';
|
||||
|
||||
onMounted(() => {
|
||||
// alert.info(
|
||||
// `<div class="text-text">
|
||||
// Stepin is a fast, light framework to Vue3 – try it out today with the
|
||||
// <span class="underline">Stepin Template Beta</span>.
|
||||
// </div>`,
|
||||
// { renderRaw: true, duration: -1 }
|
||||
// );
|
||||
});
|
||||
|
||||
const navList = [
|
||||
{
|
||||
title: 'Products',
|
||||
children: [
|
||||
{
|
||||
title: 'Stepin Template',
|
||||
list: ['Stepin Pro', 'Stepin Style', 'Stepin Admin'],
|
||||
},
|
||||
{
|
||||
title: 'Stepin',
|
||||
list: ['Stepin Vue', 'Stepin React', 'Stepin Angular'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Developers',
|
||||
children: [
|
||||
{
|
||||
title: 'Developers',
|
||||
list: ['Docs', 'Get Started', 'UI Library', 'Community', 'Open Source'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Sponsors',
|
||||
},
|
||||
{
|
||||
title: 'Business',
|
||||
children: [{ title: 'Business', list: ['Contact Us', 'Cooperation', 'Support'] }],
|
||||
},
|
||||
{
|
||||
title: 'About Us',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<ThemeProvider :color="{ middle: { 'bg-base': '#1896ff' }, primary: { DEFAULT: '#1896ff' } }" :autoAdapt="true">
|
||||
<div class="front-view flex flex-col">
|
||||
<div class="text-text flex-1">
|
||||
<div class="front-header flex items-baseline py-md px-xl">
|
||||
<div class="text-xxl text-text hover:text-text">
|
||||
<!-- <img src="@/assets/png.svg" /> -->
|
||||
dy.sync.net
|
||||
</div>
|
||||
<!-- <div style="width: calc(100% - 430px)" class="front-navigation mx-xl flex overflow-hidden items-center text-lg overflow-ellipsis whitespace-nowrap">
|
||||
<div :class="`front-nav-item flex items-center cursor-pointer mx-base ${nav.children ? 'with-list' : ''}`" v-for="nav in navList">
|
||||
<template v-if="!nav.children">
|
||||
{{ nav.title }}
|
||||
</template>
|
||||
<a-popover :mouseEnterDelay="0.1" v-else placement="bottom">
|
||||
<div class="front-nav-item-content">
|
||||
{{ nav.title }}
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="flex">
|
||||
<div class="not-[:first-child]:ml-lg" v-for="group in nav.children">
|
||||
<h3>{{ group.title }}</h3>
|
||||
<div class="cursor-pointer hover:text-text text-subtext font-light py-xs text-lg" v-for="item in group.list">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<router-link to="/login" class="h-[46px] border-transparent hover:text-text hover:border-transparent text-lg text-text">
|
||||
<LogoutOutlined class="mr-xs" />
|
||||
Sign In
|
||||
</router-link>
|
||||
<a-button class="ml-md px-lg border-text hover:border-text hover:bg-text border-2 h-[46px] hover:text-bg-container" size="large">Get Started</a-button>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="front-content ">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.front-view {
|
||||
.front-header {
|
||||
.front-nav-item {
|
||||
&.with-list .front-nav-item-content {
|
||||
&:after {
|
||||
content: '';
|
||||
@apply ~"h-[8px]" ~"w-[8px]" transition-transform ml-2 inline-block border-text border-l-0 border-t-0 border-r-2 border-b-2 border-solid ~"rotate-[-135deg]" translate-y-1/4;
|
||||
}
|
||||
&:hover {
|
||||
&:after {
|
||||
@apply ~"rotate-[45deg]" translate-y-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.front-content {
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import { LogoutOutlined } from '@ant-design/icons-vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { ThemeProvider, alert } from 'stepin';
|
||||
import http from '@/store/http';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
// console.log(router.getRoutes());
|
||||
|
||||
onMounted(() => {
|
||||
// alert.info(
|
||||
// `<div class="text-text">
|
||||
// Stepin is a fast, light framework to Vue3 – try it out today with the
|
||||
// <span class="underline">Stepin Template Beta</span>.
|
||||
// </div>`,
|
||||
// { renderRaw: true, duration: -1 }
|
||||
// );
|
||||
// console.log(router.getRoutes());
|
||||
if (http.checkAuthorization()) {
|
||||
// console.log(22222);
|
||||
router.push('/dashboard');
|
||||
} else {
|
||||
router.push('/login');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ThemeProvider :color="{ middle: { 'bg-base': '#fff','bg-container':'#fff','bg-container-light':'#fff' }, primary: { DEFAULT: '#1896ff' } }" :autoAdapt="true">
|
||||
<div class="front-view flex flex-col" style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)">
|
||||
<div class="text-xxl text-text hover:text-text" style="margin-left:20px;">
|
||||
<img src="/logo1.png" />
|
||||
douyin.sync.net
|
||||
</div>
|
||||
<div class="front-content ">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.front-view {
|
||||
.front-header {
|
||||
.front-nav-item {
|
||||
&.with-list .front-nav-item-content {
|
||||
&:after {
|
||||
content: '';
|
||||
@apply ~"h-[8px]" ~"w-[8px]" transition-transform ml-2 inline-block border-text border-l-0 border-t-0 border-r-2 border-b-2 border-solid ~"rotate-[-135deg]" translate-y-1/4;
|
||||
}
|
||||
&:hover {
|
||||
&:after {
|
||||
@apply ~"rotate-[45deg]" translate-y-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.front-content {
|
||||
height: 100vh;
|
||||
}
|
||||
.front-view {
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue';
|
||||
import { StepinHeaderAction } from 'stepin';
|
||||
import Notice from '@/components/notice/Notice.vue';
|
||||
import DayNightSwitch from '@/components/switch/DayNightSwitch.vue';
|
||||
import { BellOutlined } from '@ant-design/icons-vue';
|
||||
import Fullscreen from '../fullscreen/Fullscreen.vue';
|
||||
|
||||
defineEmits<{
|
||||
(e: 'showSetting'): void;
|
||||
}>();
|
||||
|
||||
const noticeList = reactive([
|
||||
{
|
||||
title: '消息',
|
||||
list: [
|
||||
{
|
||||
title: '影佑',
|
||||
content: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
img: 'src/assets/avatar/face-1.jpg',
|
||||
time: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
<template>
|
||||
<!-- <StepinHeaderAction>
|
||||
<a-input placeholder="开始搜索...">
|
||||
<template #prefix>
|
||||
<search-outlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</StepinHeaderAction> -->
|
||||
<StepinHeaderAction>
|
||||
<DayNightSwitch />
|
||||
</StepinHeaderAction>
|
||||
<StepinHeaderAction>
|
||||
<a-tooltip>
|
||||
<!-- placement="rightTop" -->
|
||||
<template #title><span style="color:yellow">Swagger Api</span></template>
|
||||
<a class="action-item" href="/swagger" target="_blank">
|
||||
<!-- <ApiOutlined /> -->
|
||||
<img class="gitee-logo" alt="swagger api" src="@/assets/swagger.png" />
|
||||
</a>
|
||||
</a-tooltip>
|
||||
|
||||
</StepinHeaderAction>
|
||||
<!-- <StepinHeaderAction>
|
||||
<a class="action-item" href="http://github.com/stepui/stepin-template" target="_blank">
|
||||
<GithubOutlined />
|
||||
</a>
|
||||
</StepinHeaderAction>
|
||||
<StepinHeaderAction>
|
||||
<a class="action-item" href="http://gitee.com/stepui/stepin-template" target="_blank">
|
||||
<img class="gitee-logo" src="@/assets/gitee.svg" />
|
||||
</a>
|
||||
</StepinHeaderAction> -->
|
||||
<!-- <StepinHeaderAction>
|
||||
<div class="action-item setting" @click="$emit('showSetting')">
|
||||
<SettingOutlined />
|
||||
</div>
|
||||
</StepinHeaderAction> -->
|
||||
|
||||
<!-- <a-popover placement="bottomRight">
|
||||
<StepinHeaderAction>
|
||||
<div class="action-item notice">
|
||||
<BellOutlined />
|
||||
</div>
|
||||
</StepinHeaderAction>
|
||||
<template #content>
|
||||
<Notice :data-source="noticeList" />
|
||||
</template>
|
||||
</a-popover> -->
|
||||
<!-- <StepinHeaderAction>
|
||||
<Fullscreen class="-mx-xs -my-sm h-[56px] px-xs py-sm flex items-center" target=".stepin-layout" />
|
||||
</StepinHeaderAction> -->
|
||||
</template>
|
||||
<style scoped lang="less">
|
||||
.gitee-logo {
|
||||
width: 20px;
|
||||
}
|
||||
.action-item {
|
||||
font-size: 20px;
|
||||
height: 100%;
|
||||
margin: 0 -8px;
|
||||
padding: 0 4px;
|
||||
line-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.setting {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&.notice {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>link view</div>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup></script>
|
||||
<template>
|
||||
<div class="page-footer">
|
||||
<div class="links">
|
||||
<!-- <a class="link" href="https://github.com/stepui/stepin-template" target="_blank"> Stepin 首页 </a> -->
|
||||
<!-- <a class="link" href="https://github.com/stepui/stepin-template" target="_blank">
|
||||
<GithubOutlined />
|
||||
</a> -->
|
||||
<!-- <a class="link" href="https://www.antdv.com/docs/vue/introduce-cn/" target="_blank"> Ant Design </a> -->
|
||||
</div>
|
||||
<div class="copyright">
|
||||
Copyright
|
||||
<CopyrightOutlined class="icon-copyright" />
|
||||
2023 dy.net
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.page-footer {
|
||||
text-align: center;
|
||||
@apply text-gray-400;
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.link {
|
||||
@apply hover:text-gray-400 pl-4 pr-4;
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
margin-top: 8px;
|
||||
|
||||
.icon-copyright {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as HeaderActions } from './HeaderActions.vue';
|
||||
export { default as PageFooter } from './PageFooter.vue';
|
||||
export { default as BlankView } from './BlankView.vue';
|
||||
// export { default as CommonView } from './CommonView.vue';
|
||||
export { default as FrontView } from './FrontView.vue';
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, PropType, computed } from 'vue';
|
||||
import { PaginationProps } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps({
|
||||
pagination: { type: [Object, Boolean] as PropType<PaginationProps> },
|
||||
column: { type: Number, default: 8 },
|
||||
gap: [Array<Number>, Number],
|
||||
dataSource: { type: Array<any>, default: [] },
|
||||
});
|
||||
|
||||
const _pagination = computed<PaginationProps>(() => {
|
||||
if (props.pagination && typeof props.pagination === 'boolean') {
|
||||
return {};
|
||||
}
|
||||
return props.pagination ?? {};
|
||||
});
|
||||
|
||||
const list = computed(() => {
|
||||
if (typeof props.pagination === 'boolean' && !props.pagination) {
|
||||
return props.dataSource?.slice(0);
|
||||
}
|
||||
const { current = 1, pageSize = 10 } = _pagination.value;
|
||||
let start = 0;
|
||||
let end = pageSize;
|
||||
if (props.dataSource.length > pageSize) {
|
||||
start = (current - 1) * pageSize;
|
||||
end = current * pageSize;
|
||||
}
|
||||
return props.dataSource?.slice(start, end);
|
||||
});
|
||||
|
||||
const col = computed(() => (list.value.length > 0 ? props.column : 1));
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-bind="$attrs"
|
||||
class="grid-list grid"
|
||||
:style="`${column ? 'grid-template-columns:repeat(' + col + ', minmax(0, 1fr))' : ''}`"
|
||||
>
|
||||
<template v-if="list.length > 0" v-for="item in list">
|
||||
<slot name="renderItem" :item="item">
|
||||
{{ item }}
|
||||
</slot>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-empty />
|
||||
</template>
|
||||
</div>
|
||||
<a-pagination
|
||||
class="mt-3"
|
||||
v-if="pagination"
|
||||
v-bind="{ total: dataSource?.length, ...(pagination as PaginationProps) }"
|
||||
/>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.grid-list {
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="loader relative">
|
||||
<span class="absolute -bottom-12 text-primary-500">loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="css">
|
||||
.loader {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loader:before {
|
||||
content: '';
|
||||
width: 48px;
|
||||
height: 5px;
|
||||
background: var(--color-primary-6);
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
border-radius: 50%;
|
||||
animation: shadow324 0.5s linear infinite;
|
||||
}
|
||||
|
||||
.loader:after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--color-primary-4);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 4px;
|
||||
animation: jump7456 0.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes jump7456 {
|
||||
15% {
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateY(9px) rotate(22.5deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(18px) scale(1, 0.9) rotate(45deg);
|
||||
border-bottom-right-radius: 40px;
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateY(9px) rotate(67.5deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0) rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shadow324 {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.2, 1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,529 @@
|
||||
<template>
|
||||
<div class="socket">
|
||||
<div class="gel center-gel">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c1 r1">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c2 r1">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c3 r1">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c4 r1">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c5 r1">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c6 r1">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
|
||||
<div class="gel c7 r2">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
|
||||
<div class="gel c8 r2">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c9 r2">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c10 r2">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c11 r2">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c12 r2">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c13 r2">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c14 r2">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c15 r2">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c16 r2">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c17 r2">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c18 r2">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c19 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c20 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c21 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c22 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c23 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c24 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c25 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c26 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c28 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c29 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c30 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c31 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c32 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c33 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c34 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c35 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c36 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
<div class="gel c37 r3">
|
||||
<div class="hex-brick h1"></div>
|
||||
<div class="hex-brick h2"></div>
|
||||
<div class="hex-brick h3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="css">
|
||||
.socket {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -100px;
|
||||
top: 50%;
|
||||
margin-top: -100px;
|
||||
}
|
||||
|
||||
.hex-brick {
|
||||
background: var(--color-primary-7);
|
||||
width: 30px;
|
||||
height: 17px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
animation-name: fade00;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
-webkit-animation-name: fade00;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.h2 {
|
||||
transform: rotate(60deg);
|
||||
-webkit-transform: rotate(60deg);
|
||||
}
|
||||
|
||||
.h3 {
|
||||
transform: rotate(-60deg);
|
||||
-webkit-transform: rotate(-60deg);
|
||||
}
|
||||
|
||||
.gel {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
transition: all 0.3s;
|
||||
-webkit-transition: all 0.3s;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.center-gel {
|
||||
margin-left: -15px;
|
||||
margin-top: -15px;
|
||||
animation-name: pulse00;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
-webkit-animation-name: pulse00;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
margin-left: -47px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
margin-left: -31px;
|
||||
margin-top: -43px;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
margin-left: 1px;
|
||||
margin-top: -43px;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
margin-left: 17px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
margin-left: -31px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
margin-left: 1px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
margin-left: -63px;
|
||||
margin-top: -43px;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
margin-left: 33px;
|
||||
margin-top: -43px;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
margin-left: -15px;
|
||||
margin-top: 41px;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
margin-left: -63px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
margin-left: 33px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
margin-left: -15px;
|
||||
margin-top: -71px;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
margin-left: -47px;
|
||||
margin-top: -71px;
|
||||
}
|
||||
|
||||
.c14 {
|
||||
margin-left: 17px;
|
||||
margin-top: -71px;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
margin-left: -47px;
|
||||
margin-top: 41px;
|
||||
}
|
||||
|
||||
.c16 {
|
||||
margin-left: 17px;
|
||||
margin-top: 41px;
|
||||
}
|
||||
|
||||
.c17 {
|
||||
margin-left: -79px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.c18 {
|
||||
margin-left: 49px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.c19 {
|
||||
margin-left: -63px;
|
||||
margin-top: -99px;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
margin-left: 33px;
|
||||
margin-top: -99px;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
margin-left: 1px;
|
||||
margin-top: -99px;
|
||||
}
|
||||
|
||||
.c22 {
|
||||
margin-left: -31px;
|
||||
margin-top: -99px;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
margin-left: -63px;
|
||||
margin-top: 69px;
|
||||
}
|
||||
|
||||
.c24 {
|
||||
margin-left: 33px;
|
||||
margin-top: 69px;
|
||||
}
|
||||
|
||||
.c25 {
|
||||
margin-left: 1px;
|
||||
margin-top: 69px;
|
||||
}
|
||||
|
||||
.c26 {
|
||||
margin-left: -31px;
|
||||
margin-top: 69px;
|
||||
}
|
||||
|
||||
.c27 {
|
||||
margin-left: -79px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.c28 {
|
||||
margin-left: -95px;
|
||||
margin-top: -43px;
|
||||
}
|
||||
|
||||
.c29 {
|
||||
margin-left: -95px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.c30 {
|
||||
margin-left: 49px;
|
||||
margin-top: 41px;
|
||||
}
|
||||
|
||||
.c31 {
|
||||
margin-left: -79px;
|
||||
margin-top: -71px;
|
||||
}
|
||||
|
||||
.c32 {
|
||||
margin-left: -111px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.c33 {
|
||||
margin-left: 65px;
|
||||
margin-top: -43px;
|
||||
}
|
||||
|
||||
.c34 {
|
||||
margin-left: 65px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.c35 {
|
||||
margin-left: -79px;
|
||||
margin-top: 41px;
|
||||
}
|
||||
|
||||
.c36 {
|
||||
margin-left: 49px;
|
||||
margin-top: -71px;
|
||||
}
|
||||
|
||||
.c37 {
|
||||
margin-left: 81px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.r1 {
|
||||
animation-name: pulse00;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 0.2s;
|
||||
-webkit-animation-name: pulse00;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.r2 {
|
||||
animation-name: pulse00;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 0.4s;
|
||||
-webkit-animation-name: pulse00;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.r3 {
|
||||
animation-name: pulse00;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 0.6s;
|
||||
-webkit-animation-name: pulse00;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
.r1 > .hex-brick {
|
||||
animation-name: fade00;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 0.2s;
|
||||
-webkit-animation-name: fade00;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.r2 > .hex-brick {
|
||||
animation-name: fade00;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 0.4s;
|
||||
-webkit-animation-name: fade00;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.r3 > .hex-brick {
|
||||
animation-name: fade00;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 0.6s;
|
||||
-webkit-animation-name: fade00;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
@keyframes pulse00 {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: scale(0.01);
|
||||
transform: scale(0.01);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade00 {
|
||||
0% {
|
||||
background: var(--color-primary-5);
|
||||
}
|
||||
|
||||
50% {
|
||||
background: var(--color-primary-7);
|
||||
}
|
||||
|
||||
100% {
|
||||
background: var(--color-primary-5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<div class="loader">
|
||||
<div>
|
||||
<ul>
|
||||
<li>
|
||||
<svg fill="currentColor" viewBox="0 0 90 120">
|
||||
<path
|
||||
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
|
||||
></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li>
|
||||
<svg fill="currentColor" viewBox="0 0 90 120">
|
||||
<path
|
||||
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
|
||||
></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li>
|
||||
<svg fill="currentColor" viewBox="0 0 90 120">
|
||||
<path
|
||||
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
|
||||
></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li>
|
||||
<svg fill="currentColor" viewBox="0 0 90 120">
|
||||
<path
|
||||
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
|
||||
></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li>
|
||||
<svg fill="currentColor" viewBox="0 0 90 120">
|
||||
<path
|
||||
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
|
||||
></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li>
|
||||
<svg fill="currentColor" viewBox="0 0 90 120">
|
||||
<path
|
||||
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
|
||||
></path>
|
||||
</svg>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span class="text-primary-500 text-xxxl">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="css">
|
||||
.loader {
|
||||
--background: linear-gradient(135deg, var(--color-primary-3), var(--color-primary-6));
|
||||
--shadow: var(--color-primary-7);
|
||||
--text: var(--color-primary-6);
|
||||
--page: rgba(255, 255, 255, 0.36);
|
||||
--page-fold: rgba(255, 255, 255, 0.52);
|
||||
--duration: 4s;
|
||||
width: 200px;
|
||||
height: 140px;
|
||||
position: relative;
|
||||
transform: scale(0.65);
|
||||
}
|
||||
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
--r: -6deg;
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
width: 120px;
|
||||
top: 80%;
|
||||
box-shadow: 0 16px 12px var(--shadow);
|
||||
transform: rotate(var(--r));
|
||||
}
|
||||
|
||||
.loader:before {
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
.loader:after {
|
||||
--r: 6deg;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
.loader div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 13px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
perspective: 600px;
|
||||
box-shadow: 0 4px 6px var(--shadow);
|
||||
background-image: var(--background);
|
||||
}
|
||||
|
||||
.loader div ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loader div ul li {
|
||||
--r: 180deg;
|
||||
--o: 0;
|
||||
--c: var(--page);
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
transform-origin: 100% 50%;
|
||||
color: var(--c);
|
||||
opacity: var(--o);
|
||||
transform: rotateY(var(--r));
|
||||
-webkit-animation: var(--duration) ease infinite;
|
||||
animation: var(--duration) ease infinite;
|
||||
}
|
||||
|
||||
.loader div ul li:nth-child(2) {
|
||||
--c: var(--page-fold);
|
||||
-webkit-animation-name: page-2;
|
||||
animation-name: page-2;
|
||||
}
|
||||
|
||||
.loader div ul li:nth-child(3) {
|
||||
--c: var(--page-fold);
|
||||
-webkit-animation-name: page-3;
|
||||
animation-name: page-3;
|
||||
}
|
||||
|
||||
.loader div ul li:nth-child(4) {
|
||||
--c: var(--page-fold);
|
||||
-webkit-animation-name: page-4;
|
||||
animation-name: page-4;
|
||||
}
|
||||
|
||||
.loader div ul li:nth-child(5) {
|
||||
--c: var(--page-fold);
|
||||
-webkit-animation-name: page-5;
|
||||
animation-name: page-5;
|
||||
}
|
||||
|
||||
.loader div ul li svg {
|
||||
width: 90px;
|
||||
height: 120px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loader div ul li:first-child {
|
||||
--r: 0deg;
|
||||
--o: 1;
|
||||
}
|
||||
|
||||
.loader div ul li:last-child {
|
||||
--o: 1;
|
||||
}
|
||||
|
||||
.loader span {
|
||||
display: block;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@keyframes page-2 {
|
||||
0% {
|
||||
transform: rotateY(180deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
35%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50%,
|
||||
100% {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes page-3 {
|
||||
15% {
|
||||
transform: rotateY(180deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
35% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
65%,
|
||||
100% {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes page-4 {
|
||||
30% {
|
||||
transform: rotateY(180deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
65%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
80%,
|
||||
100% {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes page-5 {
|
||||
45% {
|
||||
transform: rotateY(180deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
65% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
80%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
95%,
|
||||
100% {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, PropType } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export type Notice = {
|
||||
img: string;
|
||||
title: string;
|
||||
content: string;
|
||||
time: number;
|
||||
};
|
||||
|
||||
export type NoticeGroup = {
|
||||
title: string;
|
||||
list: Notice[];
|
||||
};
|
||||
|
||||
defineProps({
|
||||
dataSource: Array as PropType<NoticeGroup[]>,
|
||||
});
|
||||
|
||||
const active = ref(0);
|
||||
</script>
|
||||
<template>
|
||||
<a-tabs class="w-60" v-model:active="active">
|
||||
<a-tab-pane :key="i" :tab="group.title" v-for="(group, i) in dataSource">
|
||||
<div class="list max-h-40 px-2 overflow-y-auto overflow-x-hidden">
|
||||
<div class="not-[:first-child]:mt-3 flex items-center" v-for="item in group.list">
|
||||
<img class="w-11 rounded-full" :src="item.img" />
|
||||
<div class="flex flex-col ml-2">
|
||||
<div class="text-title text-xs font-semibold">
|
||||
{{ item.title }}
|
||||
<span class="text-subtext text-xs ml-1 font-normal">
|
||||
{{ dayjs(item.time).format('hh:mm') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-subtext">{{ item.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
:deep(.ant-tabs) {
|
||||
&-tab {
|
||||
@apply flex-1 justify-center;
|
||||
}
|
||||
&-nav {
|
||||
&-list {
|
||||
@apply w-full;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,289 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, Ref } from 'vue';
|
||||
import type { Component, PropType } from 'vue';
|
||||
import GridList from '../list/GridList.vue';
|
||||
import { PaginationProps } from 'ant-design-vue';
|
||||
import { debounce } from 'lodash';
|
||||
import useModelValue from '@/utils/useModelValue';
|
||||
|
||||
export interface IconSelectOption {
|
||||
label?: string;
|
||||
component: string | Component;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface IconSelectGroup {
|
||||
title: string;
|
||||
key: string | number;
|
||||
list: IconSelectOption[];
|
||||
}
|
||||
|
||||
type SelectMode = 'multiple' | 'single';
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String as PropType<SelectMode>,
|
||||
default: 'multiple',
|
||||
},
|
||||
value: {
|
||||
type: [Array<string | number>, String, Number],
|
||||
default(rawProps: any) {
|
||||
if (rawProps.mode === 'multiple' || !rawProps.mode) {
|
||||
return undefined;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
column: {
|
||||
type: Number,
|
||||
default: 8,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '选择图标,输入文字搜索...',
|
||||
},
|
||||
options: {
|
||||
type: [Array<IconSelectOption>, Array<IconSelectGroup>],
|
||||
default: [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', args: (string | number)[] | undefined | string | number): void;
|
||||
}>();
|
||||
|
||||
// 分页
|
||||
const pageBase: PaginationProps = {
|
||||
pageSize: props.column * 5,
|
||||
hideOnSinglePage: true,
|
||||
showSizeChanger: false,
|
||||
size: 'small',
|
||||
};
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
const isMultiple = computed(() => props.mode === 'multiple');
|
||||
|
||||
/**
|
||||
* 格式化分组
|
||||
*/
|
||||
const groupList = computed<(IconSelectGroup & { _searchList: IconSelectOption[]; _current: Ref<number> })[]>(() => {
|
||||
if (props.options.length === 0 || (props.options as IconSelectGroup[])[0].title !== undefined) {
|
||||
return (props.options as IconSelectGroup[]).map((group) => ({
|
||||
...group,
|
||||
_current: ref(1),
|
||||
_searchList: group.list,
|
||||
}));
|
||||
}
|
||||
return [
|
||||
{
|
||||
title: '全部图标',
|
||||
key: '__dft',
|
||||
list: props.options as IconSelectOption[],
|
||||
_current: ref(1),
|
||||
_searchList: [...props.options] as IconSelectOption[],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* icon 字典 (方便搜索和查找)
|
||||
*/
|
||||
const iconMap = computed(() => {
|
||||
const map = new Map<string | number, IconSelectOption>();
|
||||
groupList.value
|
||||
.flatMap((group) => group.list)
|
||||
.forEach((item) => {
|
||||
map.set(item.value, item);
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const { value: select } = useModelValue(
|
||||
() =>
|
||||
Array.isArray(props.value) || props.value === undefined ? (props.value as Array<string | number>) : [props.value],
|
||||
(val) => emit('update:value', isMultiple.value ? val : val?.[0])
|
||||
);
|
||||
|
||||
/**
|
||||
* 选中图标
|
||||
* @param icon
|
||||
*/
|
||||
function onSelect(icon: IconSelectOption) {
|
||||
const index = select.value?.indexOf(icon.value) ?? -1;
|
||||
if (index === -1) {
|
||||
select.value = isMultiple.value ? [...(select.value ?? []), icon.value] : [icon.value];
|
||||
} else if (isMultiple.value) {
|
||||
remove(icon.value);
|
||||
}
|
||||
if (!isMultiple.value) {
|
||||
visible.value = false;
|
||||
}
|
||||
searchValue.value = '';
|
||||
searchIcon('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除选中
|
||||
* @param icon
|
||||
*/
|
||||
function remove(iconKey: string | number) {
|
||||
const index = select.value?.findIndex((icon) => icon === iconKey) ?? -1;
|
||||
if (index >= 0) {
|
||||
select.value = select.value?.filter((icon) => icon !== iconKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索图标
|
||||
* @param keyword
|
||||
*/
|
||||
function searchIcon(keyword: string) {
|
||||
const empty = keyword === '';
|
||||
|
||||
const group = groupList.value.find((item) => item.key === active.value)!;
|
||||
|
||||
const reg = new RegExp(keyword.toLowerCase());
|
||||
|
||||
const filterIcon = (reg: RegExp, list: IconSelectOption[]) => {
|
||||
return list.filter((icon) => reg.test(icon.label!.toLocaleLowerCase()));
|
||||
};
|
||||
|
||||
group._searchList = empty ? [...group.list] : filterIcon(reg, group.list);
|
||||
|
||||
setTimeout(() => {
|
||||
groupList.value
|
||||
.filter((item) => item !== group)
|
||||
.forEach((g) => {
|
||||
g._searchList = empty ? [...g.list] : filterIcon(reg, g.list);
|
||||
});
|
||||
});
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索防抖
|
||||
*/
|
||||
const _search = debounce(searchIcon, 300);
|
||||
|
||||
/**
|
||||
* 搜索监听
|
||||
* @param value
|
||||
*/
|
||||
function onSearch(value: string) {
|
||||
searchValue.value = value;
|
||||
loading.value = true;
|
||||
_search(value);
|
||||
}
|
||||
|
||||
// 当前激活分组
|
||||
const active = ref(groupList.value[0]?.key);
|
||||
|
||||
// 搜索关键字
|
||||
const searchValue = ref('');
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
function selected(icon: IconSelectOption) {
|
||||
return select.value?.includes(icon.value);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<a-select
|
||||
@click="() => (visible = true)"
|
||||
:showSearch="true"
|
||||
mode="multiple"
|
||||
v-model:value="select"
|
||||
:open="visible"
|
||||
@blur="() => (visible = false)"
|
||||
@search="onSearch"
|
||||
:searchValue="searchValue"
|
||||
v-bind="{ placeholder }"
|
||||
allow-clear
|
||||
>
|
||||
<template #dropdownRender>
|
||||
<a-spin tip="搜索中..." :spinning="loading">
|
||||
<a-tabs
|
||||
v-model:activeKey="active"
|
||||
:class="[
|
||||
'icon-selector',
|
||||
'px-base',
|
||||
'pb-base',
|
||||
{ 'no-group pt-[12px]': groupList.length === 1 && groupList[0].key === '__dft' },
|
||||
]"
|
||||
@mousedown.prevent
|
||||
>
|
||||
<a-tab-pane :key="group.key" v-for="group in groupList">
|
||||
<template #tab>
|
||||
<a-badge
|
||||
:class="{ 'text-primary-500': active === group.key }"
|
||||
:count="group._searchList.length === group.list.length ? undefined : group._searchList.length"
|
||||
showZero
|
||||
>
|
||||
{{ group.title }}
|
||||
</a-badge>
|
||||
</template>
|
||||
<GridList
|
||||
class="icon-container"
|
||||
:dataSource="group._searchList"
|
||||
:column="column"
|
||||
:pagination="{
|
||||
...pageBase,
|
||||
current: group._current.value,
|
||||
'onUpdate:current': (val) => (group._current.value = val),
|
||||
}"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<div
|
||||
@click="onSelect(item)"
|
||||
:class="`icon-item bg-container cursor-pointer h-10 w-10 flex justify-center items-center ${
|
||||
selected(item) ? 'bg-primary-100' : ''
|
||||
}`"
|
||||
>
|
||||
<component class="icon transition" :is="item.component" />
|
||||
</div>
|
||||
</template>
|
||||
</GridList>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-spin>
|
||||
</template>
|
||||
<template #tagRender="item">
|
||||
<div class="mx-0.5 bg-bg-disabled p-1 rounded-sm flex items-center cursor-pointer">
|
||||
<component :is="iconMap.get(item.value)?.component" />
|
||||
<CloseOutlined
|
||||
v-if="isMultiple"
|
||||
class="text-subtext text-[10px] ml-1 hover:text-text"
|
||||
@click="remove(item.value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</a-select>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.icon-selector {
|
||||
:deep(.icon-container) {
|
||||
@apply p-2 text-xl grid gap-2 bg-layout rounded;
|
||||
|
||||
.icon-item {
|
||||
@apply rounded-sm border border-solid border-transparent;
|
||||
|
||||
&:hover {
|
||||
@apply border-primary-500;
|
||||
|
||||
.icon {
|
||||
@apply scale-125;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.no-group {
|
||||
:deep(.ant-tabs-nav) {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { LabelWrapper } from 'stepin';
|
||||
import { useSettingStore } from '@/store';
|
||||
const setting = useSettingStore();
|
||||
</script>
|
||||
<template>
|
||||
<div class="setting px-md">
|
||||
<LabelWrapper justify="between" label="导航模式">
|
||||
<a-radio-group v-model:value="setting.navigation" button-style="solid">
|
||||
<a-radio value="side">侧边</a-radio>
|
||||
<a-radio value="head">顶部</a-radio>
|
||||
<a-radio style="margin-right: 0" value="mix">混合</a-radio>
|
||||
</a-radio-group>
|
||||
</LabelWrapper>
|
||||
<LabelWrapper justify="between" label="多页签">
|
||||
<a-switch v-model:checked="setting.useTabs" />
|
||||
</LabelWrapper>
|
||||
<LabelWrapper justify="between" label="过滤菜单">
|
||||
<a-switch v-model:checked="setting.filterMenu" />
|
||||
</LabelWrapper>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="less"></style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Setting from './Setting.vue';
|
||||
export default Setting;
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="mini-statistic-card overflow-hidden relative min-h-[112px] bg-container inline-flex items-center justify-between drop-shadow-sm p-md border-border rounded-lg">
|
||||
<div class="statistic-main flex-1">
|
||||
<div class="statistic-title text-subtext text-xs">{{ title }}</div>
|
||||
<div class="statistic-content flex items-baseline">
|
||||
<span class="value text-title text-xxl font-bold">{{ value }}</span>
|
||||
<span class="suffix ml-1 text-xs text-green-500 font-bold">+30%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="statistic-icon absolute bottom-0 right-0">
|
||||
<slot name="icon"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
title: String,
|
||||
value: [String, Number] as PropType<string | number>,
|
||||
},
|
||||
name: 'MiniStatisticCard',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mini-statistic-card {
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="overview-title">
|
||||
{{ title }}
|
||||
<div class="subtitle">
|
||||
{{ subtitle }}
|
||||
<span :class="{ change: true, up, down }">{{ change }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OverviewTitle',
|
||||
props: {
|
||||
title: String,
|
||||
subtitle: String,
|
||||
change: [String, Number] as PropType<string | number>,
|
||||
up: { type: Boolean, required: false },
|
||||
down: { type: Boolean, required: false },
|
||||
},
|
||||
setup(props, { attrs, slots, emit }) {},
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.overview-title {
|
||||
@apply text-title font-bold text-base;
|
||||
.subtitle {
|
||||
@apply text-subtext text-xs;
|
||||
.change {
|
||||
@apply text-primary-500 text-sm ml-xs;
|
||||
&.up {
|
||||
@apply text-success-500;
|
||||
}
|
||||
&.down {
|
||||
@apply text-error-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType, watch, computed } from 'vue';
|
||||
import useModelValue from '@/utils/useModelValue';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeStore, ThemeProvider } from 'stepin/es/theme-provider';
|
||||
|
||||
export type Type = 'day' | 'night';
|
||||
|
||||
const props = defineProps({
|
||||
value: { type: String as PropType<Type> },
|
||||
nightColor: { type: String, default: '#1D1D1D' },
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: Type): void;
|
||||
}>();
|
||||
|
||||
const { value: _value } = useModelValue(
|
||||
() => props.value,
|
||||
(val) => emit('update:value', val),
|
||||
'day'
|
||||
);
|
||||
const switcher: { [key in Type]: Type } = {
|
||||
day: 'night',
|
||||
night: 'day',
|
||||
};
|
||||
|
||||
const { theme } = storeToRefs(useThemeStore());
|
||||
|
||||
// 监听主题色变换,更新缓存
|
||||
let cachedMiddleColors = cloneDeep(theme.value.color.middle);
|
||||
watch(
|
||||
theme,
|
||||
(val) => {
|
||||
if (val.color.middle['bg-base'] !== props.nightColor) {
|
||||
cachedMiddleColors = cloneDeep(val.color.middle);
|
||||
_value.value = 'day';
|
||||
} else {
|
||||
_value.value = 'night';
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 主题颜色配置
|
||||
const colorCfg = computed(() => {
|
||||
if (_value.value === 'day') {
|
||||
return { middle: cachedMiddleColors };
|
||||
}
|
||||
return { middle: { 'bg-base': props.nightColor } };
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ThemeProvider is-root :color="colorCfg">
|
||||
<div
|
||||
@click="() => (_value = switcher[_value])"
|
||||
class="bg-fill-2 day-night-switch hover:border-border relative border-border-2 text-lg rounded-full border border-solid flex items-center"
|
||||
>
|
||||
<div :class="`spot transition-[left] duration-300 h-full absolute rounded-full bg-container ${_value}`"></div>
|
||||
<IconFont :class="`day-night-switch-item ${_value === 'day' ? 'checked' : ''}`" name="icon-sun" />
|
||||
<IconFont :class="`day-night-switch-item ${_value === 'night' ? 'checked' : ''}`" name="icon-moono" />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</template>
|
||||
<style scoped lang="less">
|
||||
.day-night-switch {
|
||||
.spot {
|
||||
width: calc(50% - 1px);
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
&.night {
|
||||
left: calc(50% + 1px);
|
||||
@apply bg-layout;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
@apply z-20 bg-transparent p-xxs rounded-full text-disabled ~"last:ml-[2px]";
|
||||
&.checked {
|
||||
@apply text-text;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user