Compare commits
4 Commits
52432821b4
...
f23f8f53e6
| Author | SHA1 | Date |
|---|---|---|
|
|
f23f8f53e6 | |
|
|
b11d43a0a6 | |
|
|
c648519d42 | |
|
|
497b1a3dd4 |
10
.env
10
.env
|
|
@ -5,8 +5,12 @@
|
||||||
VITE_TITLE = 绝弹项目管理
|
VITE_TITLE = 绝弹项目管理
|
||||||
# 网站副标题
|
# 网站副标题
|
||||||
VITE_SUBTITLE = 快速开发web应用的模板工具
|
VITE_SUBTITLE = 快速开发web应用的模板工具
|
||||||
# 接口前缀 说明:参见 axios 的 baseURL
|
# 接口前缀:参见 axios 的 baseURL
|
||||||
VITE_API = /
|
VITE_API = /
|
||||||
|
# 首页路径
|
||||||
|
VITE_HOME_PATH = /home/home
|
||||||
|
# 路由模式:web(路径) hash(锚点)
|
||||||
|
VITE_HISTORY = web
|
||||||
|
|
||||||
# =====================================================================================
|
# =====================================================================================
|
||||||
# 开发设置
|
# 开发设置
|
||||||
|
|
@ -19,9 +23,5 @@ VITE_PORT = 3020
|
||||||
VITE_PROXY = http://127.0.0.1:3030/
|
VITE_PROXY = http://127.0.0.1:3030/
|
||||||
# API文档 说明:需返回符合 OPENAPI 规范的json内容
|
# API文档 说明:需返回符合 OPENAPI 规范的json内容
|
||||||
VITE_OPENAPI = http://127.0.0.1:3030/openapi.json
|
VITE_OPENAPI = http://127.0.0.1:3030/openapi.json
|
||||||
|
|
||||||
# =====================================================================================
|
|
||||||
# 构建设置
|
|
||||||
# =====================================================================================
|
|
||||||
# 文件后缀 说明:设为dev时会优先加载index.dev.vue文件,否则回退至index.vue文件
|
# 文件后缀 说明:设为dev时会优先加载index.dev.vue文件,否则回退至index.vue文件
|
||||||
VITE_EXTENSION = dev
|
VITE_EXTENSION = dev
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -41,7 +41,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 300;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.loading-tip {
|
.loading-tip {
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,139 @@
|
||||||
|
/**
|
||||||
|
* 请求函数
|
||||||
|
* @see "src/api/instance/useRequest.ts"
|
||||||
|
*/
|
||||||
|
export function useRequest<T extends PromiseFn, E = unknown>(fn: T, options: Options<T> = {}) {
|
||||||
|
type Data = Awaited<ReturnType<T>>;
|
||||||
|
type Args = Parameters<T>;
|
||||||
|
const { initialParams, initialData, retry = 0, retryDelay = 0, interval = 0 } = options;
|
||||||
|
const { onBefore, onSuccess, onError, onFinally } = options;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回数据
|
||||||
|
*/
|
||||||
|
const data = ref<Data | null>(initialData ?? null);
|
||||||
|
/**
|
||||||
|
* 请求错误
|
||||||
|
*/
|
||||||
|
const error = ref<E | null>(null);
|
||||||
|
/**
|
||||||
|
* 是否请求中
|
||||||
|
*/
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
let isCanceled = false;
|
||||||
|
let retryCount = 0;
|
||||||
|
let retryTimer = 0;
|
||||||
|
let interTimer = 0;
|
||||||
|
let latestArgs = initialParams ?? [];
|
||||||
|
|
||||||
|
const _send = async (...args: Args) => {
|
||||||
|
try {
|
||||||
|
onBefore?.(args);
|
||||||
|
loading.value = true;
|
||||||
|
const res = await fn(...args);
|
||||||
|
retryCount = 0;
|
||||||
|
if (isCanceled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
onSuccess?.(res);
|
||||||
|
data.value = res;
|
||||||
|
error.value = null;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (isCanceled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
onError?.(err);
|
||||||
|
data.value = null;
|
||||||
|
error.value = err;
|
||||||
|
if (retry > 0 && retryCount < retry) {
|
||||||
|
retryCount++;
|
||||||
|
retryTimer = setTimeout(() => _send(...args), retryDelay) as any;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
if (isCanceled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
onFinally?.();
|
||||||
|
if (!retryCount && interval > 0) {
|
||||||
|
interTimer = setTimeout(() => _send(...args), interval) as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [error.value, data.value];
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAllTimer = () => {
|
||||||
|
clearTimeout(retryTimer);
|
||||||
|
clearTimeout(interTimer);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消请求
|
||||||
|
*/
|
||||||
|
const cancel = () => {
|
||||||
|
isCanceled = true;
|
||||||
|
clearAllTimer();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送请求
|
||||||
|
*/
|
||||||
|
const send = (...args: Args) => {
|
||||||
|
isCanceled = false;
|
||||||
|
retryCount = 0;
|
||||||
|
latestArgs = args;
|
||||||
|
clearAllTimer();
|
||||||
|
return _send(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => initialParams && send(...initialParams));
|
||||||
|
onUnmounted(cancel);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
send,
|
||||||
|
cancel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
type PromiseFn = (...args: any[]) => Promise<any>;
|
type PromiseFn = (...args: any[]) => Promise<any>;
|
||||||
|
|
||||||
type Options<T extends PromiseFn = PromiseFn> = {
|
interface Options<T extends PromiseFn = PromiseFn> {
|
||||||
|
/**
|
||||||
|
* 初始请求参数
|
||||||
|
* @description 传递此参数会在挂载后立即执行
|
||||||
|
*/
|
||||||
|
initialParams?: Parameters<T>;
|
||||||
|
/**
|
||||||
|
* 默认值
|
||||||
|
* @description 与请求函数的返回值一致
|
||||||
|
*/
|
||||||
|
initialData?: Awaited<ReturnType<T>>;
|
||||||
/**
|
/**
|
||||||
* 是否显示全局的 loading
|
* 是否显示全局的 loading
|
||||||
|
* @default false
|
||||||
*/
|
*/
|
||||||
toast?: boolean | string;
|
toast?: boolean | string;
|
||||||
/**
|
|
||||||
* 是否立即执行
|
|
||||||
*/
|
|
||||||
initialParams?: boolean | Parameters<T>;
|
|
||||||
/**
|
|
||||||
* 默认值
|
|
||||||
*/
|
|
||||||
initialData?: Partial<Awaited<ReturnType<T>>>;
|
|
||||||
/**
|
/**
|
||||||
* 请求失败后重试的次数
|
* 请求失败后重试的次数
|
||||||
|
* @default 0
|
||||||
*/
|
*/
|
||||||
retry?: number;
|
retry?: number;
|
||||||
/**
|
/**
|
||||||
* 请求失败后重试的间隔(ms)
|
* 请求失败后重试的间隔(ms)
|
||||||
|
* @default 0
|
||||||
*/
|
*/
|
||||||
retryDelay?: number;
|
retryDelay?: number;
|
||||||
/**
|
/**
|
||||||
* 轮询间隔(ms)
|
* 轮询间隔(ms)
|
||||||
|
* @default 0
|
||||||
*/
|
*/
|
||||||
interval?: number;
|
interval?: number;
|
||||||
/**
|
/**
|
||||||
* 请求前回调
|
* 请求前的回调
|
||||||
*/
|
*/
|
||||||
onBefore?: (args: Parameters<T>) => void;
|
onBefore?: (args: Parameters<T>) => void;
|
||||||
/**
|
/**
|
||||||
|
|
@ -41,139 +148,4 @@ type Options<T extends PromiseFn = PromiseFn> = {
|
||||||
* 请求结束回调
|
* 请求结束回调
|
||||||
*/
|
*/
|
||||||
onFinally?: () => void;
|
onFinally?: () => void;
|
||||||
};
|
|
||||||
|
|
||||||
type State<T extends PromiseFn = PromiseFn, D = Awaited<ReturnType<T>>> = {
|
|
||||||
/**
|
|
||||||
* 请求返回的数据
|
|
||||||
*/
|
|
||||||
data: D | undefined;
|
|
||||||
/**
|
|
||||||
* 请求返回的错误
|
|
||||||
*/
|
|
||||||
error: unknown;
|
|
||||||
/**
|
|
||||||
* 请求是否中
|
|
||||||
*/
|
|
||||||
loading: boolean;
|
|
||||||
/**
|
|
||||||
* 发送请求
|
|
||||||
*/
|
|
||||||
send: (...args: Parameters<T>) => Promise<[unknown, undefined] | [undefined, D]>;
|
|
||||||
/**
|
|
||||||
* 取消请求
|
|
||||||
*/
|
|
||||||
cancel: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const log = (...args: any[]) => {
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
console.log(...args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 包装请求函数,返回响应式状态和请求方法
|
|
||||||
* @see src/api/instance/useRequest.ts
|
|
||||||
*/
|
|
||||||
export function useRequest<T extends PromiseFn>(fn: T, options: Options<T> = {}) {
|
|
||||||
const {
|
|
||||||
initialParams,
|
|
||||||
retry,
|
|
||||||
retryDelay = 0,
|
|
||||||
interval,
|
|
||||||
initialData,
|
|
||||||
onBefore,
|
|
||||||
onSuccess,
|
|
||||||
onError,
|
|
||||||
onFinally,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const state = reactive<State<T>>({
|
|
||||||
data: initialData,
|
|
||||||
error: null,
|
|
||||||
loading: false,
|
|
||||||
send: null,
|
|
||||||
cancel: null,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const inner = {
|
|
||||||
canceled: false,
|
|
||||||
retryCount: 0,
|
|
||||||
retryTimer: 0 as any,
|
|
||||||
intervalTimer: 0 as any,
|
|
||||||
latestParams: (initialParams || []) as any,
|
|
||||||
clearAllTimer: () => {
|
|
||||||
inner.retryTimer && clearTimeout(inner.retryTimer);
|
|
||||||
inner.intervalTimer && clearTimeout(inner.intervalTimer);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const _send: any = async (...args: Parameters<T>) => {
|
|
||||||
let data;
|
|
||||||
let error;
|
|
||||||
inner.retryCount && log(`retry: ${inner.retryCount}`);
|
|
||||||
try {
|
|
||||||
state.loading = true;
|
|
||||||
onBefore?.(args);
|
|
||||||
const res = await fn(...args);
|
|
||||||
inner.retryCount = 0;
|
|
||||||
if (!inner.canceled) {
|
|
||||||
onSuccess?.(res.data);
|
|
||||||
data = res.data;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (!inner.canceled) {
|
|
||||||
error = err;
|
|
||||||
onError?.(err);
|
|
||||||
if (retry && retry > 0 && inner.retryCount < retry) {
|
|
||||||
inner.retryCount++;
|
|
||||||
inner.retryTimer = setTimeout(() => {
|
|
||||||
_send(...args);
|
|
||||||
}, retryDelay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
log("finally");
|
|
||||||
state.loading = false;
|
|
||||||
state.error = error;
|
|
||||||
if (!error) {
|
|
||||||
state.data = data;
|
|
||||||
}
|
|
||||||
if (!inner.canceled) {
|
|
||||||
onFinally?.();
|
|
||||||
if (!inner.retryCount && interval && interval > 0) {
|
|
||||||
inner.intervalTimer = setTimeout(() => {
|
|
||||||
_send(...args);
|
|
||||||
}, interval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [error, data];
|
|
||||||
};
|
|
||||||
|
|
||||||
state.cancel = () => {
|
|
||||||
inner.canceled = true;
|
|
||||||
inner.clearAllTimer();
|
|
||||||
};
|
|
||||||
|
|
||||||
state.send = (...args: Parameters<T>) => {
|
|
||||||
inner.canceled = false;
|
|
||||||
inner.retryCount = 0;
|
|
||||||
inner.latestParams = args;
|
|
||||||
inner.clearAllTimer();
|
|
||||||
return _send(...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (initialParams) {
|
|
||||||
state.send(...(Array.isArray(initialParams) ? initialParams : ([] as any)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
state.cancel();
|
|
||||||
});
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineBlocker } from "../../config";
|
import { defineBlocker } from "../../core";
|
||||||
import { font } from "../components/font";
|
import { font } from "../font";
|
||||||
import { Date } from "./interface";
|
import { Date } from "./interface";
|
||||||
import Option from "./option.vue";
|
import Option from "./option.vue";
|
||||||
import Render from "./render.vue";
|
import Render from "./render.vue";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Block } from "../../config";
|
import { Block } from "../../core";
|
||||||
import { Font } from "../components/font";
|
import { Font } from "../font";
|
||||||
|
|
||||||
export interface DatePrams {
|
export interface DatePrams {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<base-option :data="data"></base-option>
|
<base-option v-model="model"></base-option>
|
||||||
<a-divider></a-divider>
|
<a-divider></a-divider>
|
||||||
<font-option :data="data.params.fontCh">
|
<font-option v-model="model.params.fontCh">
|
||||||
<a-form-item label="日期格式">
|
<a-form-item label="日期格式">
|
||||||
<a-auto-complete
|
<a-auto-complete
|
||||||
v-model="data.params.fontCh.content"
|
v-model="model.params.fontCh.content"
|
||||||
:data="FomatSuguestions"
|
:data="FomatSuguestions"
|
||||||
:allow-clear="true"
|
:allow-clear="true"
|
||||||
placeholder="例如 HH:mm:ss"
|
placeholder="例如 HH:mm:ss"
|
||||||
|
|
@ -25,17 +25,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType } from "vue";
|
|
||||||
import BaseOption from "../../components/BaseOption.vue";
|
import BaseOption from "../../components/BaseOption.vue";
|
||||||
import { FontOption } from "../components/font";
|
import { FontOption } from "../font";
|
||||||
import { Date, FomatSuguestions } from "./interface";
|
import { Date, FomatSuguestions } from "./interface";
|
||||||
|
|
||||||
defineProps({
|
const model = defineModel<Date>({ required: true });
|
||||||
data: {
|
|
||||||
type: Object as PropType<Date>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
@ -45,4 +39,4 @@ defineProps({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
../components/font
|
../components/font ../font
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { dayjs } from "@/libs/dayjs";
|
import { dayjs } from "@/libs/dayjs";
|
||||||
import { PropType } from "vue";
|
import { PropType } from "vue";
|
||||||
import { FontRender } from "../components/font";
|
import { FontRender } from "../font";
|
||||||
import { Date } from "./interface";
|
import { Date } from "./interface";
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
|
|
@ -19,4 +19,4 @@ defineProps({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
../components/font
|
../font
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,20 @@
|
||||||
<div>
|
<div>
|
||||||
<slot>
|
<slot>
|
||||||
<a-form-item label="内容">
|
<a-form-item label="内容">
|
||||||
<a-textarea v-model="data.content" placeholder="输入内容..."></a-textarea>
|
<a-textarea v-model="model.content" placeholder="输入内容..."></a-textarea>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<a-form-item label="颜色">
|
<a-form-item label="颜色">
|
||||||
<input-color v-model="data.color"></input-color>
|
<input-color v-model="model.color"></input-color>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<a-form-item label="字体">
|
<a-form-item label="字体">
|
||||||
<a-select v-model="data.family" :options="FontFamilyOptions" class="w-full overflow-hidden"> </a-select>
|
<a-select v-model="model.family" :options="FontFamilyOptions" class="w-full overflow-hidden"> </a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="大小">
|
<a-form-item label="大小">
|
||||||
<a-input-number v-model="data.size" :min="12" :step="2"> </a-input-number>
|
<a-input-number v-model="model.size" :min="12" :step="2"> </a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -24,48 +24,42 @@
|
||||||
<div class="h-8 flex items-center justify-between">
|
<div class="h-8 flex items-center justify-between">
|
||||||
<a-tag
|
<a-tag
|
||||||
class="cursor-pointer !h-7"
|
class="cursor-pointer !h-7"
|
||||||
:color="data.bold ? 'blue' : ''"
|
:color="model.bold ? 'blue' : ''"
|
||||||
:bordered="data.bold ? true : false"
|
:bordered="model.bold ? true : false"
|
||||||
@click="data.bold = !data.bold"
|
@click="model.bold = !model.bold"
|
||||||
>
|
>
|
||||||
<i class="icon-park-outline-text-bold"></i>
|
<i class="icon-park-outline-text-bold"></i>
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<a-tag
|
<a-tag
|
||||||
class="!h-7 cursor-pointer"
|
class="!h-7 cursor-pointer"
|
||||||
:color="data.italic ? 'blue' : ''"
|
:color="model.italic ? 'blue' : ''"
|
||||||
:bordered="data.italic ? true : false"
|
:bordered="model.italic ? true : false"
|
||||||
@click="data.italic = !data.italic"
|
@click="model.italic = !model.italic"
|
||||||
>
|
>
|
||||||
<i class="icon-park-outline-text-italic"></i>
|
<i class="icon-park-outline-text-italic"></i>
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<a-tag
|
<a-tag
|
||||||
class="!h-7 cursor-pointer"
|
class="!h-7 cursor-pointer"
|
||||||
:color="data.underline ? 'blue' : ''"
|
:color="model.underline ? 'blue' : ''"
|
||||||
:bordered="data.underline ? true : false"
|
:bordered="model.underline ? true : false"
|
||||||
@click="data.underline = !data.underline"
|
@click="model.underline = !model.underline"
|
||||||
>
|
>
|
||||||
<i class="icon-park-outline-text-underline"></i>
|
<i class="icon-park-outline-text-underline"></i>
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="方向">
|
<a-form-item label="方向">
|
||||||
<a-select v-model="data.align" :options="AlignOptions"></a-select>
|
<a-select v-model="model.align" :options="AlignOptions"></a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType } from "vue";
|
import InputColor from "../../components/InputColor.vue";
|
||||||
import InputColor from "../../../components/InputColor.vue";
|
|
||||||
import { AlignOptions, Font, FontFamilyOptions } from "./interface";
|
import { AlignOptions, Font, FontFamilyOptions } from "./interface";
|
||||||
|
|
||||||
defineProps({
|
const model = defineModel<Font>({ required: true });
|
||||||
data: {
|
|
||||||
type: Object as PropType<Font>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { Blocker } from "../config";
|
import { Blocker } from "../core";
|
||||||
|
|
||||||
const blockers: Record<string, Blocker> = import.meta.glob("./*/index.ts", { eager: true, import: "default" });
|
const blockers: Record<string, Blocker> = import.meta.glob(["./*/index.ts", "!./font/*"], {
|
||||||
|
eager: true,
|
||||||
|
import: "default",
|
||||||
|
});
|
||||||
const BlockerMap: Record<string, Blocker> = {};
|
const BlockerMap: Record<string, Blocker> = {};
|
||||||
|
|
||||||
for (const blocker of Object.values(blockers)) {
|
for (const blocker of Object.values(blockers)) {
|
||||||
|
|
@ -20,4 +23,3 @@ const getIcon = (type: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export { BlockerMap, getBlockerRender, getIcon, getTypeName };
|
export { BlockerMap, getBlockerRender, getIcon, getTypeName };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineBlocker } from "../../config";
|
import { defineBlocker } from "../../core";
|
||||||
import { font } from "../components/font";
|
import { font } from "../font";
|
||||||
import { Text } from "./interface";
|
import { Text } from "./interface";
|
||||||
import Option from "./option.vue";
|
import Option from "./option.vue";
|
||||||
import Render from "./render.vue";
|
import Render from "./render.vue";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Block } from "../../config";
|
import { Block } from "../../core";
|
||||||
import { Font } from "../components/font";
|
import { Font } from "../font";
|
||||||
|
|
||||||
export interface TextPrams {
|
export interface TextPrams {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<base-option :data="data"></base-option>
|
<base-option v-model="model"></base-option>
|
||||||
</div>
|
</div>
|
||||||
<a-divider></a-divider>
|
<a-divider></a-divider>
|
||||||
<div>
|
<div>
|
||||||
<div class="muti-form-item grid grid-cols-2 gap-x-4">
|
<div class="muti-form-item grid grid-cols-2 gap-x-4">
|
||||||
<a-form-item label="是否滚动">
|
<a-form-item label="是否滚动">
|
||||||
<a-radio-group type="button" v-model="data.params.marquee" class="!w-full">
|
<a-radio-group type="button" v-model="model.params.marquee" class="!w-full">
|
||||||
<a-radio :value="false">否</a-radio>
|
<a-radio :value="false">否</a-radio>
|
||||||
<a-radio :value="true">是</a-radio>
|
<a-radio :value="true">是</a-radio>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :disabled="!data.params.marquee" label="滚动速度">
|
<a-form-item :disabled="!model.params.marquee" label="滚动速度">
|
||||||
<a-input-number v-model="data.params.speed" :min="10" :step="10"></a-input-number>
|
<a-input-number v-model="model.params.speed" :min="10" :step="10"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
<a-form-item :disabled="!data.params.marquee" label="滚动方向">
|
<a-form-item :disabled="!model.params.marquee" label="滚动方向">
|
||||||
<a-radio-group type="button" v-model="data.params.direction" class="!w-full">
|
<a-radio-group type="button" v-model="model.params.direction" class="!w-full">
|
||||||
<a-radio v-for="item in DirectionOptions" :key="item.value" :value="item.value" class="dir-radio">
|
<a-radio v-for="item in DirectionOptions" :key="item.value" :value="item.value" class="dir-radio">
|
||||||
<i :class="item.icon"></i>
|
<i :class="item.icon"></i>
|
||||||
</a-radio>
|
</a-radio>
|
||||||
|
|
@ -24,21 +24,15 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
<a-divider></a-divider>
|
<a-divider></a-divider>
|
||||||
<font-option :data="data.params.fontCh"></font-option>
|
<font-option v-model="model.params.fontCh"></font-option>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType } from "vue";
|
|
||||||
import BaseOption from "../../components/BaseOption.vue";
|
import BaseOption from "../../components/BaseOption.vue";
|
||||||
import { FontOption } from "../components/font";
|
import { FontOption } from "../font";
|
||||||
import { DirectionOptions, Text } from "./interface";
|
import { DirectionOptions, Text } from "./interface";
|
||||||
|
|
||||||
defineProps({
|
const model = defineModel<Text>({ required: true });
|
||||||
data: {
|
|
||||||
type: Object as PropType<Text>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
@ -48,4 +42,5 @@ defineProps({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
../components/font
|
../components/font
|
||||||
|
../font
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType } from "vue";
|
import { PropType } from "vue";
|
||||||
import { FontRender, getFontStyle } from "../components/font";
|
import { FontRender, getFontStyle } from "../font";
|
||||||
import { Text } from "./interface";
|
import { Text } from "./interface";
|
||||||
import AniMarquee from "./marquee.vue";
|
import AniMarquee from "./marquee.vue";
|
||||||
|
|
||||||
|
|
@ -24,4 +24,4 @@ const style = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
../components/font
|
../components/font../font
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineBlocker } from "../../config";
|
import { defineBlocker } from "../../core";
|
||||||
import { font } from "../components/font";
|
import { font } from "../font";
|
||||||
import { Time } from "./interface";
|
import { Time } from "./interface";
|
||||||
import Option from "./option.vue";
|
import Option from "./option.vue";
|
||||||
import Render from "./render.vue";
|
import Render from "./render.vue";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Block } from "../../config";
|
import { Block } from "../../core";
|
||||||
import { Font } from "../components/font";
|
import { Font } from "../font";
|
||||||
|
|
||||||
export interface TimeParams {
|
export interface TimeParams {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<base-option :data="data"></base-option>
|
<base-option v-model="model"></base-option>
|
||||||
<a-divider></a-divider>
|
<a-divider></a-divider>
|
||||||
<font-option :data="data.params.fontCh">
|
<font-option v-model="model.params.fontCh">
|
||||||
<a-form-item label="时间格式">
|
<a-form-item label="时间格式">
|
||||||
<a-auto-complete
|
<a-auto-complete
|
||||||
v-model="data.params.fontCh.content"
|
v-model="model.params.fontCh.content"
|
||||||
:data="FomatSuguestions"
|
:data="FomatSuguestions"
|
||||||
:allow-clear="true"
|
:allow-clear="true"
|
||||||
placeholder="例如 HH:mm:ss"
|
placeholder="例如 HH:mm:ss"
|
||||||
|
|
@ -25,17 +25,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType } from "vue";
|
|
||||||
import BaseOption from "../../components/BaseOption.vue";
|
import BaseOption from "../../components/BaseOption.vue";
|
||||||
import { FontOption } from "../components/font";
|
import { FontOption } from "../font";
|
||||||
import { Time, FomatSuguestions } from "./interface";
|
import { Time, FomatSuguestions } from "./interface";
|
||||||
|
|
||||||
defineProps({
|
const model = defineModel<Time>({ required: true });
|
||||||
data: {
|
|
||||||
type: Object as PropType<Time>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
@ -45,4 +39,4 @@ defineProps({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
../components/font
|
../font
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<font-render :data="data.params.fontCh">
|
<font-render :data="props.data.params.fontCh">
|
||||||
{{ time }}
|
{{ time }}
|
||||||
</font-render>
|
</font-render>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { dayjs } from "@/libs/dayjs";
|
import { dayjs } from "@/libs/dayjs";
|
||||||
import { PropType, onMounted, onUnmounted, ref } from "vue";
|
import { onMounted, onUnmounted, ref } from "vue";
|
||||||
import { FontRender } from "../components/font";
|
import { FontRender } from "../font";
|
||||||
import { Time } from "./interface";
|
import { Time } from "./interface";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -34,3 +35,4 @@ onUnmounted(() => {
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
../components/font
|
../components/font
|
||||||
|
../font
|
||||||
|
|
@ -1,31 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<a-form-item label="组件名称">
|
<a-form-item label="组件名称">
|
||||||
<a-input v-model="data.title"></a-input>
|
<a-input v-model="model.title"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<a-form-item label="左侧">
|
<a-form-item label="左侧">
|
||||||
<a-input-number v-model="data.x" :min="0" :max="container.width">
|
<a-input-number v-model="model.x" :min="0" :max="container.width">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<a-tooltip content="固定水平方向">
|
<a-tooltip content="固定水平方向">
|
||||||
<i
|
<i
|
||||||
class="cursor-pointer text-gray-400 hover:text-gray-700"
|
class="cursor-pointer text-gray-400 hover:text-gray-700"
|
||||||
:class="data.xFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'"
|
:class="
|
||||||
@click="data.xFixed = !data.xFixed"
|
model.xFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'
|
||||||
|
"
|
||||||
|
@click="model.xFixed = !model.xFixed"
|
||||||
></i>
|
></i>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</a-input-number>
|
</a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="顶部">
|
<a-form-item label="顶部">
|
||||||
<a-input-number v-model="data.y" :min="0" :max="container.height">
|
<a-input-number v-model="model.y" :min="0" :max="container.height">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<a-tooltip content="固定垂直方向">
|
<a-tooltip content="固定垂直方向">
|
||||||
<i
|
<i
|
||||||
class="cursor-pointer text-gray-400 hover:text-gray-700"
|
class="cursor-pointer text-gray-400 hover:text-gray-700"
|
||||||
:class="data.yFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'"
|
:class="
|
||||||
@click="data.yFixed = !data.yFixed"
|
model.yFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'
|
||||||
|
"
|
||||||
|
@click="model.yFixed = !model.yFixed"
|
||||||
></i>
|
></i>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -35,37 +39,31 @@
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<a-form-item label="宽度">
|
<a-form-item label="宽度">
|
||||||
<a-input-number v-model="data.w" :min="0" :max="container.width"> </a-input-number>
|
<a-input-number v-model="model.w" :min="0" :max="container.width"> </a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="高度">
|
<a-form-item label="高度">
|
||||||
<a-input-number v-model="data.h" :min="0" :max="container.height"> </a-input-number>
|
<a-input-number v-model="model.h" :min="0" :max="container.height"> </a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-form-item label="背景图片">
|
<a-form-item label="背景图片">
|
||||||
<input-image v-model="data.bgImage"></input-image>
|
<input-image v-model="model.bgImage"></input-image>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label="背景颜色">
|
<a-form-item label="背景颜色">
|
||||||
<input-color v-model="data.bgColor"></input-color>
|
<input-color v-model="model.bgColor"></input-color>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType } from "vue";
|
import { Block, EditorKey } from "../core";
|
||||||
import { Block, ContextKey } from "../config";
|
|
||||||
import InputColor from "./InputColor.vue";
|
import InputColor from "./InputColor.vue";
|
||||||
import InputImage from "./InputImage.vue";
|
import InputImage from "./InputImage.vue";
|
||||||
|
|
||||||
defineProps({
|
const model = defineModel<Block>({ required: true });
|
||||||
data: {
|
const { container } = inject(EditorKey)!;
|
||||||
type: Object as PropType<Block>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { container } = inject(ContextKey)!
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
../core../core/editor
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
<template>
|
||||||
|
<a-modal :visible="visible" :fullscreen="true" :footer="false" class="ani-modal">
|
||||||
|
<div class="w-full h-full bg-slate-100 grid grid-rows-[auto_1fr] select-none">
|
||||||
|
<div class="h-13 bg-white border-b border-slate-200 z-10">
|
||||||
|
<panel-header v-model:container="container"></panel-header>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-[auto_1fr_auto] overflow-hidden">
|
||||||
|
<div class="h-full overflow-hidden bg-white shadow-[2px_0_6px_rgba(0,0,0,.05)] z-10">
|
||||||
|
<panel-left @rm-block="rmBlock" @current-block="setCurrentBlock"></panel-left>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<panel-main
|
||||||
|
v-model:rightPanelCollapsed="rightPanelCollapsed"
|
||||||
|
@add-block="addBlock"
|
||||||
|
@current-block="setCurrentBlock"
|
||||||
|
></panel-main>
|
||||||
|
</div>
|
||||||
|
<div class="h-full overflow-hidden bg-white shadow-[-2px_0_6px_rgba(0,0,0,.05)]">
|
||||||
|
<panel-right v-model:collapsed="rightPanelCollapsed" v-model:block="currentBlock"></panel-right>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<appnify-preview v-model:visible="preview"></appnify-preview>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { EditorKey, useEditor } from "../core";
|
||||||
|
import PanelHeader from "./PanelHeader.vue";
|
||||||
|
import PanelLeft from "./PanelLeft.vue";
|
||||||
|
import PanelMain from "./PanelMain.vue";
|
||||||
|
import PanelRight from "./PanelRight.vue";
|
||||||
|
import AppnifyPreview from "./EditorPreview.vue";
|
||||||
|
|
||||||
|
const visible = defineModel("visible", { default: false });
|
||||||
|
const rightPanelCollapsed = ref(false);
|
||||||
|
const leftPanelCollapsed = ref(false);
|
||||||
|
const preview = ref(false);
|
||||||
|
const editor = useEditor();
|
||||||
|
const { container, blocks, currentBlock, addBlock, rmBlock, setCurrentBlock } = editor;
|
||||||
|
|
||||||
|
const saveData = () => {
|
||||||
|
const data = {
|
||||||
|
container: container.value,
|
||||||
|
children: blocks.value,
|
||||||
|
};
|
||||||
|
const str = JSON.stringify(data);
|
||||||
|
localStorage.setItem("ANI_EDITOR_DATA", str);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
const str = localStorage.getItem("ANI_EDITOR_DATA");
|
||||||
|
if (!str) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = JSON.parse(str);
|
||||||
|
container.value = data.container;
|
||||||
|
blocks.value = data.children;
|
||||||
|
};
|
||||||
|
|
||||||
|
provide(EditorKey, editor);
|
||||||
|
onMounted(loadData);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.ani-modal {
|
||||||
|
.muti-form-item .arco-form-item .arco-form-item-label {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.arco-modal-fullscreen {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.arco-modal-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.arco-modal-body {
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.arco-tabs-nav-vertical .arco-tabs-nav-ink {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.arco-tabs-nav-vertical.arco-tabs-nav-type-line .arco-tabs-tab {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.arco-form-item-content-flex {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.arco-divider-text-left {
|
||||||
|
left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
../core/editor
|
||||||
|
|
@ -39,18 +39,10 @@
|
||||||
import { Message } from "@arco-design/web-vue";
|
import { Message } from "@arco-design/web-vue";
|
||||||
import { useFullscreen } from "@vueuse/core";
|
import { useFullscreen } from "@vueuse/core";
|
||||||
import { BlockerMap } from "../blocks";
|
import { BlockerMap } from "../blocks";
|
||||||
import { ContextKey } from "../config";
|
import { EditorKey } from "../core";
|
||||||
|
|
||||||
const { container, blocks } = inject(ContextKey)!;
|
const { container, blocks } = inject(EditorKey)!;
|
||||||
|
const visible = defineModel<boolean>("visible");
|
||||||
const props = defineProps({
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(["update:visible"]);
|
|
||||||
const el = ref<HTMLElement | null>(null);
|
const el = ref<HTMLElement | null>(null);
|
||||||
const { enter, isFullscreen, isSupported } = useFullscreen(el);
|
const { enter, isFullscreen, isSupported } = useFullscreen(el);
|
||||||
|
|
||||||
|
|
@ -58,13 +50,13 @@ watch(
|
||||||
() => isFullscreen.value,
|
() => isFullscreen.value,
|
||||||
() => {
|
() => {
|
||||||
if (!isFullscreen.value) {
|
if (!isFullscreen.value) {
|
||||||
emit("update:visible", false);
|
visible.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => visible.value,
|
||||||
(value) => {
|
(value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -79,3 +71,4 @@ watch(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
../core/editor
|
||||||
|
|
@ -43,7 +43,6 @@
|
||||||
<div class="flex-1 truncate text-gray-600" :class="{ 'text-brand-500': selectedKeys.includes(item.id) }">
|
<div class="flex-1 truncate text-gray-600" :class="{ 'text-brand-500': selectedKeys.includes(item.id) }">
|
||||||
{{ item.title }}(<span class="text-xs text-gray-400">1280 * 800</span>)
|
{{ item.title }}(<span class="text-xs text-gray-400">1280 * 800</span>)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -71,7 +70,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mockLoad } from "./mock";
|
import { mockLoad } from "../utils/mock";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|
@ -186,3 +185,4 @@ watch(
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
../utils/mock
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const model = defineModel<string>();
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
|
||||||
|
|
||||||
const model = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (value) => emit("update:modelValue", value),
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
|
||||||
|
|
@ -8,22 +8,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ImagePicker from './ImagePicker.vue';
|
import ImagePicker from "./ImagePicker.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const model = defineModel<string>();
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
|
|
||||||
const model = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (value) => emit("update:modelValue", value),
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,15 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Message } from "@arco-design/web-vue";
|
import { Message } from "@arco-design/web-vue";
|
||||||
import { ContextKey } from "../config";
|
import { Container } from "../core";
|
||||||
import AniTexter from "../panel-main/components/texter.vue";
|
import AniTexter from "./InputTexter.vue";
|
||||||
|
|
||||||
const { saveData, container } = inject(ContextKey)!;
|
|
||||||
|
|
||||||
const onSaveData = () => {
|
const onSaveData = () => {
|
||||||
saveData();
|
|
||||||
Message.success("保存成功");
|
Message.success("保存成功");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const container = defineModel<Container>("container", { required: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
../core
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!collapsed">
|
<div v-show="!collapsed">
|
||||||
<ul v-show="key === 'list'" class="list-none px-2 grid gap-2" @dragstart="onDragStart" @dragover="onDragOver">
|
<ul v-show="key === 'list'" class="list-none px-2 grid gap-2" @dragstart="onDragStart" @dragover.prevent>
|
||||||
<li
|
<li
|
||||||
v-for="item in blockList"
|
v-for="item in blockList"
|
||||||
:key="item.type"
|
:key="item.type"
|
||||||
|
|
@ -65,11 +65,11 @@
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="group h-8 w-full overflow-hidden grid grid-cols-[auto_1fr_auto] items-center gap-2 bg-gray-100 text-gray-500 px-2 py-1 rounded border border-transparent"
|
class="group h-8 w-full overflow-hidden grid grid-cols-[auto_1fr_auto] items-center gap-2 bg-gray-100 text-gray-500 px-2 py-1 rounded border border-transparent"
|
||||||
:class="{
|
:class="{
|
||||||
'!bg-brand-50': current.block === item,
|
'!bg-brand-50': currentBlock === item,
|
||||||
'!text-brand-500': current.block === item,
|
'!text-brand-500': currentBlock === item,
|
||||||
'!border-brand-300': current.block === item,
|
'!border-brand-300': currentBlock === item,
|
||||||
}"
|
}"
|
||||||
@click="setCurrentBlock(item)"
|
@click="emit('current-block', item)"
|
||||||
>
|
>
|
||||||
<div class="">
|
<div class="">
|
||||||
<i class="text-base" :class="getIcon(item.type)"></i>
|
<i class="text-base" :class="getIcon(item.type)"></i>
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
<div class="w-4">
|
<div class="w-4">
|
||||||
<i
|
<i
|
||||||
class="!hidden !group-hover:inline-block text-gray-400 hover:text-gray-700 icon-park-outline-delete !text-xs"
|
class="!hidden !group-hover:inline-block text-gray-400 hover:text-gray-700 icon-park-outline-delete !text-xs"
|
||||||
@click="onDeleteBlock($event, item)"
|
@click.prevent="emit('rm-block', item)"
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -90,13 +90,18 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { BlockerMap, getIcon } from "../blocks";
|
import { getIcon } from "../blocks";
|
||||||
import { Block, ContextKey } from "../config";
|
import { Block, EditorKey } from "../core";
|
||||||
|
|
||||||
const { blocks, current, setCurrentBlock } = inject(ContextKey)!;
|
const { blocks, currentBlock, BlockerMap } = inject(EditorKey)!;
|
||||||
const blockList = Object.values(BlockerMap);
|
const blockList = Object.values(BlockerMap);
|
||||||
const collapsed = ref(false);
|
const collapsed = ref(false);
|
||||||
const key = ref("list");
|
const key = ref<"list" | "data">("list");
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "rm-block", block: Block): void;
|
||||||
|
(event: "current-block", block: Block | null): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拖拽开始时设置数据
|
* 拖拽开始时设置数据
|
||||||
|
|
@ -104,25 +109,7 @@ const key = ref("list");
|
||||||
const onDragStart = (e: DragEvent) => {
|
const onDragStart = (e: DragEvent) => {
|
||||||
e.dataTransfer?.setData("type", (e.target as HTMLElement).dataset.type!);
|
e.dataTransfer?.setData("type", (e.target as HTMLElement).dataset.type!);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 拖拽时阻止默认行为
|
|
||||||
*/
|
|
||||||
const onDragOver = (e: Event) => {
|
|
||||||
console.log("over");
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除组件
|
|
||||||
*/
|
|
||||||
const onDeleteBlock = async (e: Event, block: Block) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const index = blocks.value.indexOf(block);
|
|
||||||
if (index > -1) {
|
|
||||||
blocks.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
../core../core/editor
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full grid grid-rows-[auto_1fr]">
|
<div class="h-full grid grid-rows-[auto_1fr]">
|
||||||
<div class="h-10">
|
<div class="h-10">
|
||||||
<ani-header :container="container"></ani-header>
|
<ani-header :container="container" v-model:rightPanelCollapsed="rightPanelCollapsed"></ani-header>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full w-full overflow-hidden p-4">
|
<div class="h-full w-full overflow-hidden p-4">
|
||||||
<div
|
<div
|
||||||
|
|
@ -10,16 +10,16 @@
|
||||||
<div
|
<div
|
||||||
class="relative"
|
class="relative"
|
||||||
:style="containerStyle"
|
:style="containerStyle"
|
||||||
|
@dragover.prevent
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
@drop="onDragDrop"
|
@drop="onDragDrop"
|
||||||
@dragover.prevent
|
@wheel="onMouseWheel"
|
||||||
@wheel="scene.onMouseWheel"
|
@mousedown="onMouseDown"
|
||||||
@mousedown="scene.onMouseDown"
|
|
||||||
>
|
>
|
||||||
<ani-block v-for="block in blocks" :key="block.id" :data="block" :container="container"></ani-block>
|
<ani-block v-for="block in blocks" :key="block.id" :data="block" :container="container"></ani-block>
|
||||||
<template v-if="refLine.active.value">
|
<template v-if="active">
|
||||||
<div
|
<div
|
||||||
v-for="line in refLine.xl.value"
|
v-for="line in xLines"
|
||||||
:key="line.y"
|
:key="line.y"
|
||||||
:style="{
|
:style="{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
}"
|
}"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
v-for="line in refLine.yl.value"
|
v-for="line in yLines"
|
||||||
:key="line.x"
|
:key="line.x"
|
||||||
:style="{
|
:style="{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
@ -50,40 +50,33 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cloneDeep } from "lodash-es";
|
import { Block, EditorKey } from "../core";
|
||||||
import { CSSProperties } from "vue";
|
import AniBlock from "./PanelMainBlock.vue";
|
||||||
import { BlockerMap } from "../blocks";
|
import AniHeader from "./PanelMainHeader.vue";
|
||||||
import { ContextKey, Scene } from "../config";
|
|
||||||
import AniBlock from "./components/block.vue";
|
|
||||||
import AniHeader from "./components/header.vue";
|
|
||||||
|
|
||||||
const { blocks, container, refLine, setCurrentBlock } = inject(ContextKey)!;
|
const rightPanelCollapsed = defineModel<boolean>("rightPanelCollapsed");
|
||||||
const scene = new Scene(container);
|
const { blocks, container, refLine, formatContainerStyle, scene } = inject(EditorKey)!;
|
||||||
|
const { onMouseDown, onMouseWheel } = scene;
|
||||||
|
const { active, xLines, yLines } = refLine;
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "add-block", type: string, x?: number, y?: number): void;
|
||||||
|
(event: "current-block", block: Block | null): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空当前组件
|
* 清空当前组件
|
||||||
*/
|
*/
|
||||||
const onClick = (e: Event) => {
|
const onClick = (e: Event) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setCurrentBlock(null);
|
emit("current-block", null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 容器样式
|
* 容器样式
|
||||||
*/
|
*/
|
||||||
const containerStyle = computed(() => {
|
const containerStyle = computed(() => formatContainerStyle(container.value));
|
||||||
const { width, height, bgColor, bgImage, zoom, x, y } = container.value;
|
|
||||||
return {
|
|
||||||
position: "absolute",
|
|
||||||
width: `${width}px`,
|
|
||||||
height: `${height}px`,
|
|
||||||
backgroundColor: bgColor,
|
|
||||||
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
|
||||||
backgroundSize: "100% 100%",
|
|
||||||
transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
|
|
||||||
} as CSSProperties;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接收拖拽并新增组件
|
* 接收拖拽并新增组件
|
||||||
|
|
@ -91,23 +84,11 @@ const containerStyle = computed(() => {
|
||||||
const onDragDrop = (e: DragEvent) => {
|
const onDragDrop = (e: DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const type = e.dataTransfer?.getData("type");
|
const type = e.dataTransfer?.getData("type");
|
||||||
if (!type) {
|
if (!type) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const blocker = BlockerMap[type];
|
emit("add-block", type, e.offsetX, e.offsetY);
|
||||||
const currentIds = blocks.value.map((item) => Number(item.id));
|
|
||||||
const maxId = currentIds.length ? Math.max.apply(null, currentIds) : 0;
|
|
||||||
const id = (maxId + 1).toString();
|
|
||||||
const title = `${blocker.title}${id}`;
|
|
||||||
blocks.value.push({
|
|
||||||
...cloneDeep(blocker.initial),
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
x: e.offsetX,
|
|
||||||
y: e.offsetY,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -127,3 +108,4 @@ const onDragDrop = (e: DragEvent) => {
|
||||||
background-position: 0 0, 10px 10px;
|
background-position: 0 0, 10px 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
../core../core/editor
|
||||||
|
|
@ -26,9 +26,9 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType } from "vue";
|
import { PropType } from "vue";
|
||||||
import { BlockerMap } from "../../blocks";
|
import { BlockerMap } from "../blocks";
|
||||||
import DragResizer from "../../components/DragResizer.vue";
|
import DragResizer from "./DragResizer.vue";
|
||||||
import { Block, Container, ContextKey } from "../../config";
|
import { Block, Container, EditorKey } from "../core";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -41,7 +41,8 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setCurrentBlock, refLine } = inject(ContextKey)!;
|
const { setCurrentBlock, refLine } = inject(EditorKey)!;
|
||||||
|
const { active, recordBlocksXY, updateRefLine } = refLine;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件样式
|
* 组件样式
|
||||||
|
|
@ -59,15 +60,12 @@ const blockStyle = computed(() => {
|
||||||
* 拖拽组件
|
* 拖拽组件
|
||||||
*/
|
*/
|
||||||
const onItemDragging = (rect: any) => {
|
const onItemDragging = (rect: any) => {
|
||||||
if (refLine.active.value) {
|
if (active.value) {
|
||||||
const { x = 0, y = 0 } = refLine.updateRefLine(rect);
|
const { offsetX = 0, offsetY = 0 } = updateRefLine(rect);
|
||||||
rect.left += x;
|
rect.left += offsetX;
|
||||||
rect.top += y;
|
rect.top += offsetY;
|
||||||
}
|
}
|
||||||
props.data.x = rect.left;
|
onItemResizing(rect);
|
||||||
props.data.y = rect.top;
|
|
||||||
props.data.w = rect.width;
|
|
||||||
props.data.h = rect.height;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -84,7 +82,7 @@ const onItemResizing = (rect: any) => {
|
||||||
* 按下鼠标
|
* 按下鼠标
|
||||||
*/
|
*/
|
||||||
const onItemMouseDown = () => {
|
const onItemMouseDown = () => {
|
||||||
refLine.active.value = true;
|
active.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -92,14 +90,14 @@ const onItemMouseDown = () => {
|
||||||
*/
|
*/
|
||||||
const onItemActivated = (block: Block) => {
|
const onItemActivated = (block: Block) => {
|
||||||
setCurrentBlock(block);
|
setCurrentBlock(block);
|
||||||
refLine.recordBlocksXY();
|
recordBlocksXY();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 松开鼠标
|
* 松开鼠标
|
||||||
*/
|
*/
|
||||||
const onItemMouseup = () => {
|
const onItemMouseup = () => {
|
||||||
refLine.active.value = false;
|
active.value = false;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -129,3 +127,4 @@ const onItemMouseup = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
../core../core/editor
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip content="预览" position="bottom">
|
<a-tooltip content="预览" position="bottom">
|
||||||
<a-button type="text" @click="preview">
|
<a-button type="text">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-play text-base !text-gray-600"></i>
|
<i class="icon-park-outline-play text-base !text-gray-600"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -58,12 +58,12 @@
|
||||||
</a-form>
|
</a-form>
|
||||||
</template>
|
</template>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
<a-tooltip :content="current.rightPanelCollapsed ? '展开' : '折叠'" position="bottom">
|
<a-tooltip :content="rightPanelCollapsed ? '展开' : '折叠'" position="bottom">
|
||||||
<a-button type="text" @click="current.rightPanelCollapsed = !current.rightPanelCollapsed">
|
<a-button type="text" @click="rightPanelCollapsed = !rightPanelCollapsed">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i
|
<i
|
||||||
class="text-base !text-gray-600"
|
class="text-base !text-gray-600"
|
||||||
:class="current.rightPanelCollapsed ? 'icon-park-outline-expand-right' : 'icon-park-outline-expand-left'"
|
:class="rightPanelCollapsed ? 'icon-park-outline-expand-right' : 'icon-park-outline-expand-left'"
|
||||||
></i>
|
></i>
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|
@ -73,12 +73,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import InputColor from "../../components/InputColor.vue";
|
import InputColor from "./InputColor.vue";
|
||||||
import InputImage from "../../components/InputImage.vue";
|
import InputImage from "./InputImage.vue";
|
||||||
import { ContextKey } from "../../config";
|
import AniTexter from "./InputTexter.vue";
|
||||||
import AniTexter from "./texter.vue";
|
import { EditorKey } from "../core";
|
||||||
|
|
||||||
const { container, blocks, current, preview, setContainerOrigin } = inject(ContextKey)!;
|
const { container, blocks, setContainerOrigin } = inject(EditorKey)!;
|
||||||
|
|
||||||
|
const rightPanelCollapsed = defineModel<boolean>("rightPanelCollapsed");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
../core/editor
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full w-[248px] overflow-hidden" :style="`display: ${current.rightPanelCollapsed ? 'none' : 'block'}`">
|
<div class="h-full w-[248px] overflow-hidden" :style="`display: ${collapsed ? 'none' : 'block'}`">
|
||||||
<div v-if="current.block" class="p-3">
|
<div v-if="model" class="p-3">
|
||||||
<a-radio-group type="button" default-value="1" class="w-full mb-2">
|
|
||||||
<a-radio value="1">属性</a-radio>
|
|
||||||
<a-radio value="2">文本</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
<a-form :model="{}" layout="vertical">
|
<a-form :model="{}" layout="vertical">
|
||||||
<div class="muti-form-item mt-2">
|
<div class="muti-form-item mt-2">
|
||||||
<component :is="BlockerMap[current.block.type].option" :data="current.block" />
|
<component :is="BlockerMap[model.type].option" v-model="model" />
|
||||||
</div>
|
</div>
|
||||||
</a-form>
|
</a-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -19,9 +15,11 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { BlockerMap } from "../blocks";
|
import { BlockerMap } from "../blocks";
|
||||||
import { ContextKey } from "../config";
|
import { Block } from "../core";
|
||||||
|
|
||||||
const { current } = inject(ContextKey)!;
|
const collapsed = defineModel<boolean>("collapsed");
|
||||||
|
const model = defineModel<Block | null>("block");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
../core
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import { InjectionKey, Ref } from "vue";
|
|
||||||
import { Block } from "./block";
|
|
||||||
import { Container } from "./container";
|
|
||||||
import { ReferenceLine } from "./ref-line";
|
|
||||||
|
|
||||||
export interface Current {
|
|
||||||
block: Block | null;
|
|
||||||
rightPanelCollapsed: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Context {
|
|
||||||
/**
|
|
||||||
* 运行时数据
|
|
||||||
*/
|
|
||||||
current: Ref<Current>;
|
|
||||||
/**
|
|
||||||
* 组件列表
|
|
||||||
*/
|
|
||||||
blocks: Ref<Block[]>;
|
|
||||||
/**
|
|
||||||
* 画布配置
|
|
||||||
*/
|
|
||||||
container: Ref<Container>;
|
|
||||||
/**
|
|
||||||
* 参考线
|
|
||||||
*/
|
|
||||||
refLine: ReferenceLine;
|
|
||||||
/**
|
|
||||||
* 设置当前组件
|
|
||||||
* @param block 组件
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
setCurrentBlock: (block: Block | null) => void;
|
|
||||||
/**
|
|
||||||
* 容器自适应
|
|
||||||
*/
|
|
||||||
setContainerOrigin: () => void;
|
|
||||||
/**
|
|
||||||
* 保存数据
|
|
||||||
*/
|
|
||||||
saveData: () => void;
|
|
||||||
/**
|
|
||||||
* 加载数据
|
|
||||||
*/
|
|
||||||
loadData: () => void;
|
|
||||||
/**
|
|
||||||
* 预览
|
|
||||||
*/
|
|
||||||
preview: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ContextKey = Symbol("ContextKey") as InjectionKey<Context>;
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { Ref } from "vue";
|
|
||||||
import { Container } from "./container";
|
|
||||||
import { Block } from "./block";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
export class Editor {
|
|
||||||
public container: Ref<Container> = {} as Ref<Container>;
|
|
||||||
public content: Ref<Block> = {} as Ref<Block>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
import { Ref } from "vue";
|
|
||||||
import { Container } from ".";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 场景
|
|
||||||
* @description 处理平移和缩放事件
|
|
||||||
*/
|
|
||||||
export class Scene {
|
|
||||||
private startX = 0;
|
|
||||||
private startY = 0;
|
|
||||||
private cacheX = 0;
|
|
||||||
private cacheY = 0;
|
|
||||||
public minZoom = 0.5;
|
|
||||||
public maxZoom = 10;
|
|
||||||
public zoomStep = 0.1;
|
|
||||||
|
|
||||||
constructor(private container: Ref<Container>) {
|
|
||||||
this.onMouseDown = this.onMouseDown.bind(this);
|
|
||||||
this.onMouseMove = this.onMouseMove.bind(this);
|
|
||||||
this.onMouseUp = this.onMouseUp.bind(this);
|
|
||||||
this.onMouseWheel = this.onMouseWheel.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseDown(e: MouseEvent) {
|
|
||||||
this.startX = e.x;
|
|
||||||
this.startY = e.y;
|
|
||||||
this.cacheX = this.container.value.x;
|
|
||||||
this.cacheY = this.container.value.y;
|
|
||||||
window.addEventListener("mousemove", this.onMouseMove);
|
|
||||||
window.addEventListener("mouseup", this.onMouseUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseMove(e: MouseEvent) {
|
|
||||||
this.container.value.x = this.cacheX + (e.x - this.startX);
|
|
||||||
this.container.value.y = this.cacheY + (e.y - this.startY);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseUp() {
|
|
||||||
window.removeEventListener("mousemove", this.onMouseMove);
|
|
||||||
window.removeEventListener("mouseup", this.onMouseUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseWheel(e: WheelEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const el = e.currentTarget as HTMLElement;
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const x = (e.clientX - rect.x) / this.container.value.zoom;
|
|
||||||
const y = (e.clientY - rect.y) / this.container.value.zoom;
|
|
||||||
const delta = -e.deltaY > 0 ? this.zoomStep : -this.zoomStep;
|
|
||||||
|
|
||||||
this.container.value.zoom += delta;
|
|
||||||
if (this.container.value.zoom < this.minZoom) {
|
|
||||||
this.container.value.zoom = this.minZoom;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.container.value.zoom > this.maxZoom) {
|
|
||||||
this.container.value.zoom = this.maxZoom;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.container.value.x += -x * delta + el.offsetWidth * (delta / 2);
|
|
||||||
this.container.value.y += -y * delta + el.offsetHeight * (delta / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
/**
|
||||||
|
* 组件参数
|
||||||
|
*/
|
||||||
export interface Block<T = any> {
|
export interface Block<T = any> {
|
||||||
/**
|
/**
|
||||||
* 组件ID
|
* 组件ID
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { Component } from "vue";
|
import { Component } from "vue";
|
||||||
import { Block } from "./block";
|
import { Block } from "./block";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件配置
|
||||||
|
*/
|
||||||
export interface Blocker<T = any> {
|
export interface Blocker<T = any> {
|
||||||
/**
|
/**
|
||||||
* 组件名称
|
* 组件名称
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
/**
|
||||||
|
* 画布配置
|
||||||
|
*/
|
||||||
export interface Container {
|
export interface Container {
|
||||||
/**
|
/**
|
||||||
* 容器id
|
* 容器id
|
||||||
|
|
@ -40,3 +43,19 @@ export interface Container {
|
||||||
*/
|
*/
|
||||||
bgColor: string;
|
bgColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 画布默认配置
|
||||||
|
*/
|
||||||
|
export const defaultContainer: Container = {
|
||||||
|
id: 11,
|
||||||
|
title: "国庆节喜庆版式设计",
|
||||||
|
description: "适用于国庆节1日-7日间上午9:00-10:00播出的版式设计",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
zoom: 0.7,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
bgImage: "",
|
||||||
|
bgColor: "#ffffff",
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
import { Container, defaultContainer } from "./container";
|
||||||
|
import { Block } from "./block";
|
||||||
|
import { useReferenceLine } from "./ref-line";
|
||||||
|
import { BlockerMap } from "../blocks";
|
||||||
|
import { cloneDeep } from "lodash-es";
|
||||||
|
import { CSSProperties, InjectionKey } from "vue";
|
||||||
|
import { useScene } from "./scene";
|
||||||
|
|
||||||
|
export const useEditor = () => {
|
||||||
|
/**
|
||||||
|
* 画布设置
|
||||||
|
*/
|
||||||
|
const container = ref<Container>({ ...defaultContainer });
|
||||||
|
/**
|
||||||
|
* 组件列表
|
||||||
|
*/
|
||||||
|
const blocks = ref<Block[]>([]);
|
||||||
|
/**
|
||||||
|
* 选中组件
|
||||||
|
*/
|
||||||
|
const currentBlock = ref<Block | null>(null);
|
||||||
|
/**
|
||||||
|
* 参考线
|
||||||
|
*/
|
||||||
|
const refLine = useReferenceLine(blocks, currentBlock);
|
||||||
|
/**
|
||||||
|
* 画布移动和缩放
|
||||||
|
*/
|
||||||
|
const scene = useScene(container);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加组件
|
||||||
|
* @param type 组件类型
|
||||||
|
* @param x 横坐标
|
||||||
|
* @param y 纵坐标
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const addBlock = (type: string, x = 0, y = 0) => {
|
||||||
|
if (!type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const blocker = BlockerMap[type];
|
||||||
|
if (!blocker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ids = blocks.value.map((i) => Number(i.id));
|
||||||
|
const maxId = ids.length ? Math.max.apply(null, ids) : 0;
|
||||||
|
const id = (maxId + 1).toString();
|
||||||
|
const title = `${blocker.title}${id}`;
|
||||||
|
blocks.value.push({
|
||||||
|
...cloneDeep(blocker.initial),
|
||||||
|
id,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除组件
|
||||||
|
* @param block 组件
|
||||||
|
*/
|
||||||
|
const rmBlock = (block: Block) => {
|
||||||
|
const index = blocks.value.indexOf(block);
|
||||||
|
if (index > -1) {
|
||||||
|
blocks.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化组件样式
|
||||||
|
* @param block 组件
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const formatBlockStyle = (block: Block) => {
|
||||||
|
const { bgColor, bgImage } = block;
|
||||||
|
return {
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化容器样式
|
||||||
|
* @param container 容器
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const formatContainerStyle = (container: Container) => {
|
||||||
|
const { width, height, bgColor, bgImage, zoom, x, y } = container;
|
||||||
|
return {
|
||||||
|
position: "absolute",
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
|
||||||
|
} as CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前选中组件
|
||||||
|
* @param block 组件
|
||||||
|
*/
|
||||||
|
const setCurrentBlock = (block: Block | null) => {
|
||||||
|
for (const item of blocks.value) {
|
||||||
|
item.actived = false;
|
||||||
|
}
|
||||||
|
if (!block) {
|
||||||
|
currentBlock.value = null;
|
||||||
|
} else {
|
||||||
|
block.actived = true;
|
||||||
|
currentBlock.value = block;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置画布坐标和比例
|
||||||
|
*/
|
||||||
|
const setContainerOrigin = () => {
|
||||||
|
container.value.x = 0;
|
||||||
|
container.value.y = 0;
|
||||||
|
const el = document.querySelector(".juetan-editor-container");
|
||||||
|
if (el) {
|
||||||
|
const { width, height } = el.getBoundingClientRect();
|
||||||
|
const wZoom = width / container.value.width;
|
||||||
|
const hZoom = height / container.value.width;
|
||||||
|
const zoom = Math.floor((wZoom > hZoom ? wZoom : hZoom) * 10000) / 10000;
|
||||||
|
container.value.zoom = zoom;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
container,
|
||||||
|
blocks,
|
||||||
|
currentBlock,
|
||||||
|
refLine,
|
||||||
|
scene,
|
||||||
|
BlockerMap,
|
||||||
|
setCurrentBlock,
|
||||||
|
setContainerOrigin,
|
||||||
|
addBlock,
|
||||||
|
rmBlock,
|
||||||
|
formatBlockStyle,
|
||||||
|
formatContainerStyle,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditorKey = Symbol("EditorKey") as InjectionKey<ReturnType<typeof useEditor>>;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export * from "./block";
|
export * from "./block";
|
||||||
export * from "./blocker";
|
export * from "./blocker";
|
||||||
export * from "./container";
|
export * from "./container";
|
||||||
export * from "./context";
|
|
||||||
export * from "./ref-line";
|
export * from "./ref-line";
|
||||||
export * from "./scene";
|
export * from "./scene";
|
||||||
|
export * from "./editor";
|
||||||
|
|
@ -1,71 +1,58 @@
|
||||||
import { Ref } from "vue";
|
import { Ref } from "vue";
|
||||||
import { Block } from "./block";
|
import { Block } from "./block";
|
||||||
import { Current } from "./context";
|
import { getClosestValInSortedArr } from "../utils/closest";
|
||||||
import { getClosestValInSortedArr } from "./util";
|
|
||||||
|
|
||||||
export interface DragRect {
|
|
||||||
left: number;
|
|
||||||
top: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 参考线管理
|
* 组件参考线
|
||||||
|
* @param blocks 组件列表
|
||||||
|
* @param current 当前组件
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
export class ReferenceLine {
|
export const useReferenceLine = (blocks: Ref<Block[]>, current: Ref<Block | null>) => {
|
||||||
private xYsMap = new Map<number, number[]>();
|
let xYsMap = new Map<number, number[]>();
|
||||||
private yXsMap = new Map<number, number[]>();
|
let yXsMap = new Map<number, number[]>();
|
||||||
private sortedXs: number[] = [];
|
let sortedXs: number[] = [];
|
||||||
private sortedYs: number[] = [];
|
let sortedYs: number[] = [];
|
||||||
private xLines: { y: number; xs: number[] }[] = [];
|
const active = ref(false);
|
||||||
private yLines: { x: number; ys: number[] }[] = [];
|
const xLines = ref<{ x: number; y: number; w: number }[]>([]);
|
||||||
public active = ref(false);
|
const yLines = ref<{ x: number; y: number; h: number }[]>([]);
|
||||||
public xl = ref<{ x: number; y: number; w: number }[]>([]);
|
|
||||||
public yl = ref<{ x: number; y: number; h: number }[]>([]);
|
|
||||||
|
|
||||||
constructor(private blocks: Ref<Block[]>, private current: Ref<Current>) {
|
|
||||||
this.updateRefLine = this.updateRefLine.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录所有组件坐标
|
* 记录所有组件坐标
|
||||||
*/
|
*/
|
||||||
recordBlocksXY() {
|
const recordBlocksXY = () => {
|
||||||
this.clear();
|
clear();
|
||||||
const { xYsMap, yXsMap, blocks, current } = this;
|
|
||||||
for (const block of blocks.value) {
|
for (const block of blocks.value) {
|
||||||
if (block === current.value.block) {
|
if (block === current.value) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const { minX, minY, midX, midY, maxX, maxY } = this.getBlockBox(block);
|
const { minX, minY, midX, midY, maxX, maxY } = getBlockBox(block);
|
||||||
|
addBoxToMap(xYsMap, minX, [minY, maxY]);
|
||||||
|
addBoxToMap(xYsMap, midX, [minY, maxY]);
|
||||||
|
addBoxToMap(xYsMap, maxX, [minY, maxY]);
|
||||||
|
|
||||||
this.addBoxToMap(xYsMap, minX, [minY, maxY]);
|
addBoxToMap(yXsMap, minY, [minX, maxX]);
|
||||||
this.addBoxToMap(xYsMap, midX, [minY, maxY]);
|
addBoxToMap(yXsMap, midY, [minX, maxX]);
|
||||||
this.addBoxToMap(xYsMap, maxX, [minY, maxY]);
|
addBoxToMap(yXsMap, maxY, [minX, maxX]);
|
||||||
|
|
||||||
this.addBoxToMap(yXsMap, minY, [minX, maxX]);
|
|
||||||
this.addBoxToMap(yXsMap, midY, [minX, maxX]);
|
|
||||||
this.addBoxToMap(yXsMap, maxY, [minX, maxX]);
|
|
||||||
}
|
}
|
||||||
this.sortedXs = Array.from(xYsMap.keys()).sort((a, b) => a - b);
|
sortedXs = Array.from(xYsMap.keys()).sort((a, b) => a - b);
|
||||||
this.sortedYs = Array.from(yXsMap.keys()).sort((a, b) => a - b);
|
sortedYs = Array.from(yXsMap.keys()).sort((a, b) => a - b);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加组件坐标
|
* 添加组件坐标
|
||||||
*/
|
*/
|
||||||
addBoxToMap(map: Map<number, number[]>, xOrY: number, xsOrYs: number[]) {
|
const addBoxToMap = (map: Map<number, number[]>, xOrY: number, xsOrYs: number[]) => {
|
||||||
if (!map.get(xOrY)) {
|
if (!map.get(xOrY)) {
|
||||||
map.set(xOrY, []);
|
map.set(xOrY, []);
|
||||||
}
|
}
|
||||||
map.get(xOrY)?.push(...xsOrYs);
|
map.get(xOrY)?.push(...xsOrYs);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取组件左中右坐标
|
* 获取组件左中右坐标
|
||||||
*/
|
*/
|
||||||
getBlockBox(block: Block) {
|
const getBlockBox = (block: Block) => {
|
||||||
const { x, y, w, h } = block ?? {};
|
const { x, y, w, h } = block ?? {};
|
||||||
return {
|
return {
|
||||||
minX: x,
|
minX: x,
|
||||||
|
|
@ -75,12 +62,12 @@ export class ReferenceLine {
|
||||||
maxX: x + w,
|
maxX: x + w,
|
||||||
maxY: y + h,
|
maxY: y + h,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取组件左中右坐标
|
* 获取组件左中右坐标
|
||||||
*/
|
*/
|
||||||
getRectBox(rect: DragRect) {
|
const getRectBox = (rect: DragRect) => {
|
||||||
const { left: x, top: y, width: w, height: h } = rect;
|
const { left: x, top: y, width: w, height: h } = rect;
|
||||||
return {
|
return {
|
||||||
minX: x,
|
minX: x,
|
||||||
|
|
@ -90,18 +77,18 @@ export class ReferenceLine {
|
||||||
maxX: x + w,
|
maxX: x + w,
|
||||||
maxY: y + h,
|
maxY: y + h,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理数据
|
* 清理数据
|
||||||
*/
|
*/
|
||||||
clear() {
|
function clear() {
|
||||||
this.xYsMap.clear();
|
xYsMap.clear();
|
||||||
this.yXsMap.clear();
|
yXsMap.clear();
|
||||||
this.sortedXs = [];
|
sortedXs = [];
|
||||||
this.sortedYs = [];
|
sortedYs = [];
|
||||||
this.xl.value = [];
|
xLines.value = [];
|
||||||
this.yl.value = [];
|
yLines.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -112,18 +99,16 @@ export class ReferenceLine {
|
||||||
* 5. 更新组件坐标
|
* 5. 更新组件坐标
|
||||||
* 6. 绘制参考线段
|
* 6. 绘制参考线段
|
||||||
*/
|
*/
|
||||||
updateRefLine(rect: DragRect) {
|
function updateRefLine(rect: DragRect) {
|
||||||
this.xLines = [];
|
const allXLines = [];
|
||||||
this.yLines = [];
|
const allYLines = [];
|
||||||
const box = this.getRectBox(rect);
|
const box = getRectBox(rect);
|
||||||
const { xYsMap, yXsMap, sortedXs, sortedYs } = this;
|
let offsetX: number | undefined;
|
||||||
|
let offsetY: number | undefined;
|
||||||
if (!sortedXs.length && !sortedYs.length) {
|
if (!sortedXs.length && !sortedYs.length) {
|
||||||
return { x: 0, y: 0 };
|
return { x: 0, y: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
let offsetX: number | undefined = undefined;
|
|
||||||
let offsetY: number | undefined = undefined;
|
|
||||||
|
|
||||||
// 离最近X的距离
|
// 离最近X的距离
|
||||||
const closetMinX = getClosestValInSortedArr(sortedXs, box.minX);
|
const closetMinX = getClosestValInSortedArr(sortedXs, box.minX);
|
||||||
const closetMidX = getClosestValInSortedArr(sortedXs, box.midX);
|
const closetMidX = getClosestValInSortedArr(sortedXs, box.midX);
|
||||||
|
|
@ -185,19 +170,19 @@ export class ReferenceLine {
|
||||||
|
|
||||||
if (offsetX !== undefined) {
|
if (offsetX !== undefined) {
|
||||||
if (isEqualNum(0, closetMinX - targetBox.minX)) {
|
if (isEqualNum(0, closetMinX - targetBox.minX)) {
|
||||||
this.yLines.push({
|
allYLines.push({
|
||||||
x: closetMinX,
|
x: closetMinX,
|
||||||
ys: [targetBox.minY, targetBox.maxY, ...(xYsMap.get(closetMinX) ?? [])],
|
ys: [targetBox.minY, targetBox.maxY, ...(xYsMap.get(closetMinX) ?? [])],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isEqualNum(0, closetMidX - targetBox.midX)) {
|
if (isEqualNum(0, closetMidX - targetBox.midX)) {
|
||||||
this.yLines.push({
|
allYLines.push({
|
||||||
x: closetMidX,
|
x: closetMidX,
|
||||||
ys: [targetBox.midX, ...(xYsMap.get(closetMidX) ?? [])],
|
ys: [targetBox.midX, ...(xYsMap.get(closetMidX) ?? [])],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isEqualNum(0, closetMaxX - targetBox.maxX)) {
|
if (isEqualNum(0, closetMaxX - targetBox.maxX)) {
|
||||||
this.yLines.push({
|
allYLines.push({
|
||||||
x: closetMaxX,
|
x: closetMaxX,
|
||||||
ys: [targetBox.minY, targetBox.maxY, ...(xYsMap.get(closetMaxX) ?? [])],
|
ys: [targetBox.minY, targetBox.maxY, ...(xYsMap.get(closetMaxX) ?? [])],
|
||||||
});
|
});
|
||||||
|
|
@ -206,45 +191,62 @@ export class ReferenceLine {
|
||||||
|
|
||||||
if (offsetY !== undefined) {
|
if (offsetY !== undefined) {
|
||||||
if (isEqualNum(0, closetMinY - targetBox.minY)) {
|
if (isEqualNum(0, closetMinY - targetBox.minY)) {
|
||||||
this.xLines.push({
|
allXLines.push({
|
||||||
y: closetMinY,
|
y: closetMinY,
|
||||||
xs: [targetBox.minX, targetBox.maxX, ...(yXsMap.get(closetMinY) ?? [])],
|
xs: [targetBox.minX, targetBox.maxX, ...(yXsMap.get(closetMinY) ?? [])],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isEqualNum(0, closetMidY - targetBox.midY)) {
|
if (isEqualNum(0, closetMidY - targetBox.midY)) {
|
||||||
this.xLines.push({
|
allXLines.push({
|
||||||
y: closetMidY,
|
y: closetMidY,
|
||||||
xs: [targetBox.midX, ...(yXsMap.get(closetMidY) ?? [])],
|
xs: [targetBox.midX, ...(yXsMap.get(closetMidY) ?? [])],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isEqualNum(0, closetMaxY - targetBox.maxY)) {
|
if (isEqualNum(0, closetMaxY - targetBox.maxY)) {
|
||||||
this.xLines.push({
|
allXLines.push({
|
||||||
y: closetMaxY,
|
y: closetMaxY,
|
||||||
xs: [targetBox.minX, targetBox.maxX, ...(yXsMap.get(closetMaxY) ?? [])],
|
xs: [targetBox.minX, targetBox.maxX, ...(yXsMap.get(closetMaxY) ?? [])],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const yl: any[] = [];
|
const yls: any[] = [];
|
||||||
for (const line of this.yLines) {
|
for (const line of allYLines) {
|
||||||
const y = Math.min(...line.ys);
|
const y = Math.min(...line.ys);
|
||||||
const h = Math.max(...line.ys) - y;
|
const h = Math.max(...line.ys) - y;
|
||||||
yl.push({ x: line.x, y, h });
|
yls.push({ x: line.x, y, h });
|
||||||
}
|
}
|
||||||
|
|
||||||
const xl: any[] = [];
|
const xls: any[] = [];
|
||||||
for (const line of this.xLines) {
|
for (const line of allXLines) {
|
||||||
const x = Math.min(...line.xs);
|
const x = Math.min(...line.xs);
|
||||||
const w = Math.max(...line.xs) - x;
|
const w = Math.max(...line.xs) - x;
|
||||||
xl.push({ y: line.y, x, w });
|
xls.push({ y: line.y, x, w });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.yl.value = yl;
|
yLines.value = yls;
|
||||||
this.xl.value = xl;
|
xLines.value = xls;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: offsetX,
|
offsetX,
|
||||||
y: offsetY,
|
offsetY,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
active,
|
||||||
|
xLines,
|
||||||
|
yLines,
|
||||||
|
recordBlocksXY,
|
||||||
|
updateRefLine,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DragRect {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ReferenceLine = ReturnType<typeof useReferenceLine>;
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { Ref } from "vue";
|
||||||
|
import { Container } from ".";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 画布场景
|
||||||
|
* @description 处理平移和缩放事件
|
||||||
|
*/
|
||||||
|
export const useScene = (container: Ref<Container>) => {
|
||||||
|
const minZoom = 0.5;
|
||||||
|
const maxZoom = 10;
|
||||||
|
const zoomStep = 0.1;
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let cacheX = 0;
|
||||||
|
let cacheY = 0;
|
||||||
|
|
||||||
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
|
startX = e.x;
|
||||||
|
startY = e.y;
|
||||||
|
cacheX = container.value.x;
|
||||||
|
cacheY = container.value.y;
|
||||||
|
window.addEventListener("mousemove", onMouseMove);
|
||||||
|
window.addEventListener("mouseup", onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
container.value.x = cacheX + (e.x - startX);
|
||||||
|
container.value.y = cacheY + (e.y - startY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
window.removeEventListener("mousemove", onMouseMove);
|
||||||
|
window.removeEventListener("mouseup", onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseWheel = (e: WheelEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const el = e.currentTarget as HTMLElement;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - rect.x) / container.value.zoom;
|
||||||
|
const y = (e.clientY - rect.y) / container.value.zoom;
|
||||||
|
const delta = -e.deltaY > 0 ? zoomStep : -zoomStep;
|
||||||
|
|
||||||
|
container.value.zoom += delta;
|
||||||
|
if (container.value.zoom < minZoom) {
|
||||||
|
container.value.zoom = minZoom;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (container.value.zoom > maxZoom) {
|
||||||
|
container.value.zoom = maxZoom;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.value.x += -x * delta + el.offsetWidth * (delta / 2);
|
||||||
|
container.value.y += -y * delta + el.offsetHeight * (delta / 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onMouseDown,
|
||||||
|
onMouseWheel,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Editor from "./components/Editor.vue";
|
||||||
|
|
||||||
|
export { Editor }
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
<template>
|
|
||||||
<a-modal :visible="visible" :fullscreen="true" :footer="false" class="ani-modal">
|
|
||||||
<div class="w-full h-full bg-slate-100 grid grid-rows-[auto_1fr] select-none">
|
|
||||||
<div class="h-13 bg-white border-b border-slate-200 z-10">
|
|
||||||
<panel-header></panel-header>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-[auto_1fr_auto] overflow-hidden">
|
|
||||||
<div class="h-full overflow-hidden bg-white shadow-[2px_0_6px_rgba(0,0,0,.05)] z-10">
|
|
||||||
<panel-left></panel-left>
|
|
||||||
</div>
|
|
||||||
<div class="w-full h-full">
|
|
||||||
<panel-main></panel-main>
|
|
||||||
</div>
|
|
||||||
<div class="h-full overflow-hidden bg-white shadow-[-2px_0_6px_rgba(0,0,0,.05)]">
|
|
||||||
<panel-right></panel-right>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<appnify-preview v-model:visible="preview"></appnify-preview>
|
|
||||||
</a-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Block, Container, ContextKey, ReferenceLine } from "./config";
|
|
||||||
import PanelHeader from "./panel-header/index.vue";
|
|
||||||
import PanelLeft from "./panel-left/index.vue";
|
|
||||||
import PanelMain from "./panel-main/index.vue";
|
|
||||||
import PanelRight from "./panel-right/index.vue";
|
|
||||||
import AppnifyPreview from "./preview/index.vue";
|
|
||||||
|
|
||||||
const visible = defineModel("visible", { default: false });
|
|
||||||
const preview = ref(false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 运行时上下文
|
|
||||||
*/
|
|
||||||
const current = ref({
|
|
||||||
block: null as Block | null,
|
|
||||||
rightPanelCollapsed: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 组件列表
|
|
||||||
*/
|
|
||||||
const blocks = ref<Block[]>([]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 参考线
|
|
||||||
*/
|
|
||||||
const refLine = new ReferenceLine(blocks, current);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 画布容器
|
|
||||||
*/
|
|
||||||
const container = ref<Container>({
|
|
||||||
id: 11,
|
|
||||||
title: "国庆节喜庆版式设计",
|
|
||||||
description: "适用于国庆节1日-7日间上午9:00-10:00播出的版式设计",
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
zoom: 0.7,
|
|
||||||
width: 1920,
|
|
||||||
height: 1080,
|
|
||||||
bgImage: "",
|
|
||||||
bgColor: "#ffffff",
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadData();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存数据
|
|
||||||
*/
|
|
||||||
const saveData = () => {
|
|
||||||
const data = {
|
|
||||||
container: container.value,
|
|
||||||
children: blocks.value,
|
|
||||||
};
|
|
||||||
const str = JSON.stringify(data);
|
|
||||||
localStorage.setItem("ANI_EDITOR_DATA", str);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载数据
|
|
||||||
*/
|
|
||||||
const loadData = async () => {
|
|
||||||
const str = localStorage.getItem("ANI_EDITOR_DATA");
|
|
||||||
if (!str) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = JSON.parse(str);
|
|
||||||
container.value = data.container;
|
|
||||||
blocks.value = data.children;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置当前选中的组件
|
|
||||||
*/
|
|
||||||
const setCurrentBlock = (block: Block | null) => {
|
|
||||||
for (const block of blocks.value) {
|
|
||||||
block.actived = false;
|
|
||||||
}
|
|
||||||
if (!block) {
|
|
||||||
current.value.block = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
block.actived = true;
|
|
||||||
current.value.block = block;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 恢复画布到原始比例和远点
|
|
||||||
*/
|
|
||||||
const setContainerOrigin = () => {
|
|
||||||
container.value.x = 0;
|
|
||||||
container.value.y = 0;
|
|
||||||
const el = document.querySelector(".juetan-editor-container");
|
|
||||||
if (el) {
|
|
||||||
const { width, height } = el.getBoundingClientRect();
|
|
||||||
const wZoom = width / container.value.width;
|
|
||||||
const hZoom = height / container.value.width;
|
|
||||||
const zoom = Math.floor((wZoom > hZoom ? wZoom : hZoom) * 10000) / 10000;
|
|
||||||
container.value.zoom = zoom;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提供上下文注入
|
|
||||||
*/
|
|
||||||
provide(ContextKey, {
|
|
||||||
current,
|
|
||||||
container,
|
|
||||||
blocks,
|
|
||||||
refLine,
|
|
||||||
setCurrentBlock,
|
|
||||||
setContainerOrigin,
|
|
||||||
loadData,
|
|
||||||
saveData,
|
|
||||||
preview() {
|
|
||||||
preview.value = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.ani-modal {
|
|
||||||
.muti-form-item .arco-form-item .arco-form-item-label {
|
|
||||||
// color: #899;
|
|
||||||
// font-size: 12px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.arco-modal-fullscreen {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.arco-modal-header {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.arco-modal-body {
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.arco-tabs-nav-vertical .arco-tabs-nav-ink {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.arco-tabs-nav-vertical.arco-tabs-nav-type-line .arco-tabs-tab {
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
.arco-form-item-content-flex {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.arco-divider-text-left {
|
|
||||||
left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const sleep = (time: number) => {
|
export const sleep = (time: number) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve();
|
resolve();
|
||||||
|
|
@ -10,7 +10,7 @@ export default defineComponent({
|
||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
() => {
|
() => {
|
||||||
selectedKeys.value = route.matched.map((i) => i.path);
|
selectedKeys.value = route.matched.map((i) => i.aliasOf?.path ?? i.path);
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
@ -31,7 +31,9 @@ export default defineComponent({
|
||||||
const icon = route.icon ? () => <i class={route.icon} /> : null;
|
const icon = route.icon ? () => <i class={route.icon} /> : null;
|
||||||
const node: any = route.children?.length ? (
|
const node: any = route.children?.length ? (
|
||||||
<>
|
<>
|
||||||
<div class="px-2"><a-divider margin={6} class="!border-slate-100"></a-divider></div>
|
<div class="px-2">
|
||||||
|
<a-divider margin={6} class="!border-slate-100"></a-divider>
|
||||||
|
</div>
|
||||||
{this.renderItem(route?.children)}
|
{this.renderItem(route?.children)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ const onSubmitForm = async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const res = await api.auth.login(model);
|
const res = await api.auth.login(model);
|
||||||
userStore.setAccessToken(res.data.data as unknown as string);
|
userStore.setAccessToken(res.data.data);
|
||||||
Notification.success({
|
Notification.success({
|
||||||
title: "登陆提示",
|
title: "登陆提示",
|
||||||
content: `欢迎,您已成功登陆系统!`,
|
content: `欢迎,您已成功登陆系统!`,
|
||||||
|
|
@ -124,7 +124,7 @@ const onSubmitForm = async () => {
|
||||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
.login-left {
|
.login-left {
|
||||||
background: rgb(var(--primary-6)) url(/src/pages/_login/image-br.svg) no-repeat center center/90% auto;
|
background: rgb(var(--primary-6)) url(./image-br.svg) no-repeat center center/90% auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import { Table, useTable } from "@/components";
|
import { Table, useTable } from "@/components";
|
||||||
import aniEditor from "@/components/editor/index.vue";
|
import { Editor as aniEditor } from "@/components/editor";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import { createColumn, updateColumn, useAniTable } from "@/components";
|
import { createColumn, updateColumn, useAniTable } from "@/components";
|
||||||
import { MenuTypes, MenuType } from "@/constants/menu";
|
import { MenuType, MenuTypes } from "@/constants/menu";
|
||||||
import { flatedMenus } from "@/router";
|
import { flatMenus } from "@/router";
|
||||||
import { listToTree } from "@/utils/listToTree";
|
import { listToTree } from "@/utils/listToTree";
|
||||||
|
|
||||||
const menuArr = flatedMenus.map((i) => ({ label: i.title, value: i.id }));
|
const menuArr = flatMenus.map((i) => ({ label: i.title, value: i.id }));
|
||||||
|
|
||||||
const expanded = ref(false);
|
const expanded = ref(false);
|
||||||
const toggleExpand = () => {
|
const toggleExpand = () => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { api } from "@/api";
|
||||||
|
import { store, useUserStore } from "@/store";
|
||||||
|
import { useMenuStore } from "@/store/menu";
|
||||||
|
import { treeFind } from "@/utils/listToTree";
|
||||||
|
import { Notification } from "@arco-design/web-vue";
|
||||||
|
import { Router } from "vue-router";
|
||||||
|
import { menus } from "../menus";
|
||||||
|
import { APP_HOME_NAME } from "../routes/base";
|
||||||
|
import { APP_ROUTE_NAME, routes } from "../routes/page";
|
||||||
|
|
||||||
|
const WHITE_LIST = ["/:all(.*)*"];
|
||||||
|
const UNSIGNIN_LIST = ["/login"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限守卫
|
||||||
|
* @param to 路由
|
||||||
|
* @description store不能放在外面,否则 pinia-plugin-peristedstate 插件会失效
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useAuthGuard(router: Router) {
|
||||||
|
api.expireHandler = () => {
|
||||||
|
const userStore = useUserStore(store);
|
||||||
|
const redirect = router.currentRoute.value.path;
|
||||||
|
userStore.clearUser();
|
||||||
|
router.push({ path: "/login", query: { redirect } });
|
||||||
|
};
|
||||||
|
|
||||||
|
router.beforeEach(async function (to) {
|
||||||
|
const userStore = useUserStore(store);
|
||||||
|
const menuStore = useMenuStore(store);
|
||||||
|
if (to.meta.auth?.some((i) => i === "*")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (WHITE_LIST.includes(to.path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (UNSIGNIN_LIST.includes(to.path)) {
|
||||||
|
if (!userStore.accessToken) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Notification.warning({
|
||||||
|
title: "跳转提示",
|
||||||
|
content: `您已登陆,如需重新登陆请退出后再操作!`,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!userStore.accessToken) {
|
||||||
|
return { path: "/login", query: { redirect: to.path } };
|
||||||
|
}
|
||||||
|
if (!menuStore.menus.length) {
|
||||||
|
menuStore.setMenus(menus);
|
||||||
|
menuStore.setHome(import.meta.env.VITE_HOME_PATH);
|
||||||
|
for (const route of routes) {
|
||||||
|
router.addRoute(route);
|
||||||
|
}
|
||||||
|
const home = treeFind(routes, (i) => i.path === menuStore.home);
|
||||||
|
if (home) {
|
||||||
|
const route = { ...home, name: APP_HOME_NAME, alias: "/" };
|
||||||
|
router.removeRoute(home.name!);
|
||||||
|
router.addRoute(APP_ROUTE_NAME, route);
|
||||||
|
return router.replace(to.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { store, useUserStore } from "@/store";
|
|
||||||
import { Message } from "@arco-design/web-vue";
|
|
||||||
import { NavigationGuardWithThis } from "vue-router";
|
|
||||||
|
|
||||||
const whitelist = ["/:all(.*)*"];
|
|
||||||
const signoutlist = ["/login"];
|
|
||||||
|
|
||||||
export const authGuard: NavigationGuardWithThis<undefined> = async function (to) {
|
|
||||||
// 放在外面,pinia-plugin-peristedstate 插件会失效
|
|
||||||
const userStore = useUserStore(store);
|
|
||||||
if (to.meta?.auth === false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (whitelist.includes(to.path) || to.name === "_all") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (signoutlist.includes(to.path)) {
|
|
||||||
if (userStore.accessToken) {
|
|
||||||
Message.warning(`提示:您已登陆,如需重新请退出后再操作!`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!userStore.accessToken) {
|
|
||||||
return {
|
|
||||||
path: "/login",
|
|
||||||
query: {
|
|
||||||
redirect: to.path,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { NProgress } from "@/libs/nprogress";
|
|
||||||
import { useAppStore } from "@/store";
|
|
||||||
import { NavigationGuardWithThis, NavigationHookAfter } from "vue-router";
|
|
||||||
|
|
||||||
const routeMap = new Map<string, boolean>();
|
|
||||||
|
|
||||||
const before: NavigationGuardWithThis<undefined> = function (to) {
|
|
||||||
NProgress.start();
|
|
||||||
if (routeMap.get(to.fullPath)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const appStore = useAppStore();
|
|
||||||
appStore.setPageLoading(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const after: NavigationHookAfter = function (to) {
|
|
||||||
NProgress.done();
|
|
||||||
if (routeMap.get(to.fullPath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const appStore = useAppStore();
|
|
||||||
setTimeout(() => {
|
|
||||||
appStore.setPageLoading(false);
|
|
||||||
routeMap.set(to.fullPath, true);
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const progressGuard = {
|
|
||||||
before,
|
|
||||||
after,
|
|
||||||
};
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import { store, useAppStore } from "@/store";
|
|
||||||
import { NavigationHookAfter } from "vue-router";
|
|
||||||
|
|
||||||
export const titleGuard: NavigationHookAfter = function (to) {
|
|
||||||
const appStore = useAppStore(store);
|
|
||||||
const title = to.meta.title || appStore.title;
|
|
||||||
const subtitle = appStore.subtitle;
|
|
||||||
document.title = `${title} | ${subtitle}`;
|
|
||||||
};
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { NProgress } from "@/libs/nprogress";
|
||||||
|
import { useAppStore } from "@/store";
|
||||||
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
|
const routeMap = new Map<string, boolean>();
|
||||||
|
|
||||||
|
export function useProgressGard(router: Router) {
|
||||||
|
router.beforeEach(function (to) {
|
||||||
|
NProgress.start();
|
||||||
|
if (routeMap.get(to.fullPath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const appStore = useAppStore();
|
||||||
|
appStore.setPageLoading(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.afterEach(function (to) {
|
||||||
|
NProgress.done();
|
||||||
|
if (routeMap.get(to.fullPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const appStore = useAppStore();
|
||||||
|
setTimeout(() => {
|
||||||
|
appStore.setPageLoading(false);
|
||||||
|
routeMap.set(to.fullPath, true);
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { store, useAppStore } from "@/store";
|
||||||
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
|
export function useTitleGuard(router: Router) {
|
||||||
|
router.beforeEach(function (to) {
|
||||||
|
const appStore = useAppStore(store);
|
||||||
|
const title = to.meta.title || appStore.title;
|
||||||
|
const subtitle = appStore.subtitle;
|
||||||
|
document.title = `${title} | ${subtitle}`;
|
||||||
|
});
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./menus";
|
export * from "./menus";
|
||||||
export * from "./router";
|
export * from "./router";
|
||||||
export * from "./routes";
|
export * from "./routes/page";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { RouteRecordRaw } from "vue-router";
|
import { RouteRecordRaw } from "vue-router";
|
||||||
import { appRoutes } from "../routes";
|
import { appRoutes } from "../routes/page";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 菜单项类型
|
* 菜单项类型
|
||||||
*/
|
*/
|
||||||
interface MenuItem {
|
export interface MenuItem {
|
||||||
id: string;
|
id: string;
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
|
@ -23,38 +23,32 @@ interface MenuItem {
|
||||||
function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
|
function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
|
||||||
const items: MenuItem[] = [];
|
const items: MenuItem[] = [];
|
||||||
|
|
||||||
routes.forEach((route) => {
|
for (const route of routes) {
|
||||||
|
const { meta = {}, parentMeta, path } = route as any;
|
||||||
|
const { title, sort, icon } = meta;
|
||||||
|
let id = path;
|
||||||
let paths = route.path.split("/");
|
let paths = route.path.split("/");
|
||||||
let id = route.path;
|
|
||||||
let parentId = paths.slice(0, -1).join("/");
|
let parentId = paths.slice(0, -1).join("/");
|
||||||
|
if (parentMeta) {
|
||||||
if ((route as any).parentMeta) {
|
const { title, icon, sort } = parentMeta;
|
||||||
id = `${route.path}/index`;
|
id = `${path}/index`;
|
||||||
parentId = route.path;
|
parentId = path;
|
||||||
items.push({
|
items.push({
|
||||||
id: route.path,
|
title,
|
||||||
|
icon,
|
||||||
|
sort,
|
||||||
|
path,
|
||||||
|
id: path,
|
||||||
parentId: paths.slice(0, -1).join("/"),
|
parentId: paths.slice(0, -1).join("/"),
|
||||||
path: `${route.path}`,
|
|
||||||
title: (route as any).parentMeta.title,
|
|
||||||
icon: (route as any).parentMeta.icon,
|
|
||||||
sort: (route as any).parentMeta.sort,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const p = paths.slice(0, -1).join("/");
|
const p = paths.slice(0, -1).join("/");
|
||||||
if (routes.some((i) => i.path === p && (i as any).parentMeta)) {
|
if (routes.some((i) => i.path === p) && parentMeta) {
|
||||||
parentId = p;
|
parentId = p;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
items.push({ id, title, parentId, path, icon, sort });
|
||||||
items.push({
|
}
|
||||||
id,
|
|
||||||
parentId,
|
|
||||||
path: route.path,
|
|
||||||
sort: route.meta?.sort,
|
|
||||||
title: route.meta?.title,
|
|
||||||
icon: route.meta?.icon,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
@ -68,18 +62,18 @@ function listToTree(list: MenuItem[]) {
|
||||||
const map: Record<string, MenuItem> = {};
|
const map: Record<string, MenuItem> = {};
|
||||||
const tree: MenuItem[] = [];
|
const tree: MenuItem[] = [];
|
||||||
|
|
||||||
list.forEach((item) => {
|
for (const item of list) {
|
||||||
map[item.id] = item;
|
map[item.id] = item;
|
||||||
});
|
}
|
||||||
|
|
||||||
list.forEach((item) => {
|
for (const item of list) {
|
||||||
const parent = map[item.parentId as string];
|
const parent = map[item.parentId as string];
|
||||||
if (parent) {
|
if (parent) {
|
||||||
(parent.children || (parent.children = [])).push(item);
|
(parent.children || (parent.children = [])).push(item);
|
||||||
} else {
|
} else {
|
||||||
tree.push(item);
|
tree.push(item);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
@ -102,31 +96,17 @@ function sort<T extends { children?: T[]; [key: string]: any }>(routes: T[], key
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 转换路由为树形菜单项,并排序
|
|
||||||
* @param routes 路由配置
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function transformToMenuItems(routes: RouteRecordRaw[]) {
|
|
||||||
const menus = routesToItems(routes);
|
|
||||||
const tree = listToTree(menus);
|
|
||||||
return sort(tree);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扁平化的菜单
|
* 扁平化的菜单
|
||||||
*/
|
*/
|
||||||
const flatedMenus = routesToItems(appRoutes);
|
export const flatMenus = routesToItems(appRoutes);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 树结构菜单
|
* 树结构菜单
|
||||||
*/
|
*/
|
||||||
const treeMenus = listToTree(flatedMenus);
|
export const treeMenus = listToTree(flatMenus);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 排序过的树级菜单
|
* 排序过的树级菜单
|
||||||
*/
|
*/
|
||||||
const menus = sort(treeMenus);
|
export const menus = sort(treeMenus);
|
||||||
|
|
||||||
export { menus, treeMenus, flatedMenus };
|
|
||||||
export type { MenuItem };
|
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,29 @@
|
||||||
import { createRouter, createWebHashHistory } from "vue-router";
|
import { createRouter } from "vue-router";
|
||||||
import { authGuard } from "../guards/guard-auth";
|
import { useAuthGuard } from "../guards/auth";
|
||||||
import { progressGuard } from "../guards/guard-progress";
|
import { useProgressGard } from "../guards/progress";
|
||||||
import { titleGuard } from "../guards/guard-title";
|
import { useTitleGuard } from "../guards/title";
|
||||||
import { routes } from "../routes";
|
import { baseRoutes } from "../routes/base";
|
||||||
import { api } from "@/api";
|
import { historyMode } from "./util";
|
||||||
import { store, useUserStore } from "@/store";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由实例
|
||||||
|
*/
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: historyMode(),
|
||||||
routes: [
|
routes: [...baseRoutes],
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
redirect: "/home/home",
|
|
||||||
},
|
|
||||||
...routes,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach(progressGuard.before);
|
/**
|
||||||
router.afterEach(progressGuard.after);
|
* 进度条守卫
|
||||||
router.beforeEach(authGuard);
|
*/
|
||||||
router.afterEach(titleGuard);
|
useProgressGard(router);
|
||||||
|
|
||||||
api.expireHandler = () => {
|
/**
|
||||||
const userStore = useUserStore(store);
|
* 权限守卫
|
||||||
userStore.clearUser();
|
*/
|
||||||
const redirect = router.currentRoute.value.path;
|
useAuthGuard(router);
|
||||||
router.push({ path: "/login", query: { redirect } });
|
|
||||||
};
|
/**
|
||||||
|
* 标题守卫
|
||||||
|
*/
|
||||||
|
useTitleGuard(router);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { createWebHashHistory, createWebHistory } from "vue-router";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模式映射
|
||||||
|
*/
|
||||||
|
const HistoryMap = {
|
||||||
|
web: createWebHistory,
|
||||||
|
hash: createWebHashHistory,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由模式
|
||||||
|
*/
|
||||||
|
export function historyMode() {
|
||||||
|
const mode = HistoryMap[import.meta.env.VITE_HISTORY];
|
||||||
|
return mode();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { RouteRecordRaw } from "vue-router";
|
||||||
|
|
||||||
|
export const APP_HOME_NAME = "__APP_HOME__";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基本路由
|
||||||
|
*/
|
||||||
|
export const baseRoutes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: APP_HOME_NAME,
|
||||||
|
component: () => "Home Page",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import generatedRoutes from "virtual:generated-pages";
|
import generatedRoutes from "virtual:generated-pages";
|
||||||
import { RouteRecordRaw } from "vue-router";
|
import { RouteRecordRaw } from "vue-router";
|
||||||
|
|
||||||
const APP_ROUTE_NAME = "_layout";
|
export const TOP_ROUTE_PREF = "_";
|
||||||
|
export const APP_ROUTE_NAME = "_layout";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换一维路由为二维路由
|
* 转换路由
|
||||||
* @description 以 _ 开头的路由为顶级路由,其余为应用路由
|
* @description 以 _ 开头的路由为顶级路由,其余为应用路由
|
||||||
*/
|
*/
|
||||||
const transformRoutes = (routes: RouteRecordRaw[]) => {
|
const transformRoutes = (routes: RouteRecordRaw[]) => {
|
||||||
|
|
@ -12,11 +13,11 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
|
||||||
const appRoutes: RouteRecordRaw[] = [];
|
const appRoutes: RouteRecordRaw[] = [];
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
if ((route.name as string)?.startsWith("_")) {
|
if ((route.name as string)?.startsWith(TOP_ROUTE_PREF)) {
|
||||||
if (route.name === APP_ROUTE_NAME) {
|
if (route.name === APP_ROUTE_NAME) {
|
||||||
route.children = appRoutes;
|
route.children = appRoutes;
|
||||||
}
|
}
|
||||||
route.path = route.path.replace("_", "");
|
route.path = route.path.replace(TOP_ROUTE_PREF, "");
|
||||||
topRoutes.push(route);
|
topRoutes.push(route);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -1,43 +1,13 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
interface PageTag {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
path: string;
|
|
||||||
closable?: boolean;
|
|
||||||
closible?: boolean;
|
|
||||||
actived?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAppStore = defineStore({
|
export const useAppStore = defineStore({
|
||||||
id: "app",
|
id: "app",
|
||||||
state: () => ({
|
state: (): AppStore => ({
|
||||||
/**
|
|
||||||
* 是否为暗模式
|
|
||||||
*/
|
|
||||||
isDarkMode: false,
|
isDarkMode: false,
|
||||||
/**
|
|
||||||
* 站点标题
|
|
||||||
*/
|
|
||||||
title: import.meta.env.VITE_TITLE,
|
title: import.meta.env.VITE_TITLE,
|
||||||
/**
|
|
||||||
* 站点副标题
|
|
||||||
*/
|
|
||||||
subtitle: import.meta.env.VITE_SUBTITLE,
|
subtitle: import.meta.env.VITE_SUBTITLE,
|
||||||
/**
|
|
||||||
* 页面是否加载中,用于路由首次加载
|
|
||||||
*/
|
|
||||||
pageLoding: false,
|
pageLoding: false,
|
||||||
pageTags: [
|
pageTags: [],
|
||||||
{
|
|
||||||
id: "/",
|
|
||||||
title: "首页",
|
|
||||||
path: "/",
|
|
||||||
closable: false,
|
|
||||||
closible: false,
|
|
||||||
actived: false,
|
|
||||||
},
|
|
||||||
] as PageTag[],
|
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,6 +16,7 @@ export const useAppStore = defineStore({
|
||||||
toggleDark() {
|
toggleDark() {
|
||||||
this.isDarkMode ? this.setLight() : this.setDark();
|
this.isDarkMode ? this.setLight() : this.setDark();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换为亮模式
|
* 切换为亮模式
|
||||||
*/
|
*/
|
||||||
|
|
@ -54,6 +25,7 @@ export const useAppStore = defineStore({
|
||||||
document.body.classList.remove("dark");
|
document.body.classList.remove("dark");
|
||||||
this.isDarkMode = false;
|
this.isDarkMode = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换为暗模式
|
* 切换为暗模式
|
||||||
*/
|
*/
|
||||||
|
|
@ -62,12 +34,14 @@ export const useAppStore = defineStore({
|
||||||
document.body.classList.add("dark");
|
document.body.classList.add("dark");
|
||||||
this.isDarkMode = true;
|
this.isDarkMode = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置页面加载loading
|
* 设置页面加载loading
|
||||||
*/
|
*/
|
||||||
setPageLoading(loading: boolean) {
|
setPageLoading(loading: boolean) {
|
||||||
this.pageLoding = loading;
|
this.pageLoding = loading;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加页面标签
|
* 添加页面标签
|
||||||
* @param tag 标签
|
* @param tag 标签
|
||||||
|
|
@ -83,14 +57,13 @@ export const useAppStore = defineStore({
|
||||||
actived: false,
|
actived: false,
|
||||||
...tag,
|
...tag,
|
||||||
});
|
});
|
||||||
console.log(this.pageTags);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除页面标签
|
* 移除页面标签
|
||||||
* @param tag 标签
|
* @param tag 标签
|
||||||
*/
|
*/
|
||||||
delPageTag(tag: PageTag) {
|
delPageTag(tag: PageTag) {
|
||||||
console.log("del page tag");
|
|
||||||
const index = this.pageTags.findIndex((i) => i.id === tag.id);
|
const index = this.pageTags.findIndex((i) => i.id === tag.id);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
this.pageTags.splice(index, 1);
|
this.pageTags.splice(index, 1);
|
||||||
|
|
@ -99,3 +72,35 @@ export const useAppStore = defineStore({
|
||||||
},
|
},
|
||||||
persist: !import.meta.env.DEV,
|
persist: !import.meta.env.DEV,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface AppStore {
|
||||||
|
/**
|
||||||
|
* 是否为暗模式
|
||||||
|
*/
|
||||||
|
isDarkMode: boolean;
|
||||||
|
/**
|
||||||
|
* 站点标题
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* 站点副标题
|
||||||
|
*/
|
||||||
|
subtitle: string;
|
||||||
|
/**
|
||||||
|
* 页面是否加载中,用于路由首次加载
|
||||||
|
*/
|
||||||
|
pageLoding: boolean;
|
||||||
|
/**
|
||||||
|
* 标签数组
|
||||||
|
*/
|
||||||
|
pageTags: PageTag[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageTag {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
path: string;
|
||||||
|
closable?: boolean;
|
||||||
|
closible?: boolean;
|
||||||
|
actived?: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { MenuItem } from "@/router";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const useMenuStore = defineStore({
|
||||||
|
id: "menu",
|
||||||
|
state: (): MenuStore => {
|
||||||
|
return {
|
||||||
|
menus: [],
|
||||||
|
home: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
/**
|
||||||
|
* 设置菜单
|
||||||
|
*/
|
||||||
|
setMenus(menus: MenuItem[]) {
|
||||||
|
this.menus = menus;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置首页
|
||||||
|
* @param path 路径
|
||||||
|
*/
|
||||||
|
setHome(path: string) {
|
||||||
|
this.home = path;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface MenuStore {
|
||||||
|
/**
|
||||||
|
* 路由列表
|
||||||
|
*/
|
||||||
|
menus: MenuItem[];
|
||||||
|
/**
|
||||||
|
* 首页路径
|
||||||
|
*/
|
||||||
|
home: string;
|
||||||
|
}
|
||||||
|
|
@ -2,4 +2,5 @@ import { createPinia } from "pinia";
|
||||||
import persistedstatePlugin from "pinia-plugin-persistedstate";
|
import persistedstatePlugin from "pinia-plugin-persistedstate";
|
||||||
|
|
||||||
export const store = createPinia();
|
export const store = createPinia();
|
||||||
store.use(persistedstatePlugin);
|
|
||||||
|
store.use(persistedstatePlugin);
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,13 @@ import { defineStore } from "pinia";
|
||||||
|
|
||||||
export const useUserStore = defineStore({
|
export const useUserStore = defineStore({
|
||||||
id: "user",
|
id: "user",
|
||||||
state: () => {
|
state: (): UserStore => {
|
||||||
return {
|
return {
|
||||||
/**
|
|
||||||
* 用户ID
|
|
||||||
*/
|
|
||||||
id: 0,
|
id: 0,
|
||||||
/**
|
|
||||||
* 登录用户名
|
|
||||||
*/
|
|
||||||
username: "juetan",
|
username: "juetan",
|
||||||
/**
|
|
||||||
* 用户昵称
|
|
||||||
*/
|
|
||||||
nickname: "绝弹",
|
nickname: "绝弹",
|
||||||
/** `
|
|
||||||
* 用户头像地址
|
|
||||||
*/
|
|
||||||
avatar: "https://github.com/juetan.png",
|
avatar: "https://github.com/juetan.png",
|
||||||
/**
|
|
||||||
* JWT令牌
|
|
||||||
*/
|
|
||||||
accessToken: "",
|
accessToken: "",
|
||||||
/**
|
|
||||||
* 刷新令牌
|
|
||||||
*/
|
|
||||||
refreshToken: undefined,
|
refreshToken: undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
@ -38,7 +20,11 @@ export const useUserStore = defineStore({
|
||||||
this.accessToken = token;
|
this.accessToken = token;
|
||||||
},
|
},
|
||||||
|
|
||||||
setAccessToken(token: string) {
|
/**
|
||||||
|
* 设置访问令牌
|
||||||
|
* @param token 令牌
|
||||||
|
*/
|
||||||
|
setAccessToken(token?: string) {
|
||||||
this.accessToken = token;
|
this.accessToken = token;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -52,13 +38,41 @@ export const useUserStore = defineStore({
|
||||||
/**
|
/**
|
||||||
* 设置用户信息
|
* 设置用户信息
|
||||||
*/
|
*/
|
||||||
setUser(user: any) {
|
setUser(user: Partial<UserStore>) {
|
||||||
this.id = user.id;
|
const { id, username, nickname, avatar, accessToken } = user;
|
||||||
this.username = user.username;
|
id && (this.id = id);
|
||||||
this.nickname = user.nickname;
|
username && (this.username = username);
|
||||||
this.avatar = user.avatar;
|
nickname && (this.nickname = nickname);
|
||||||
this.accessToken = user.token;
|
avatar && (this.avatar = avatar);
|
||||||
|
accessToken && (this.accessToken = accessToken);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
persist: true,
|
persist: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface UserStore {
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* 登录用户名
|
||||||
|
*/
|
||||||
|
username: string;
|
||||||
|
/**
|
||||||
|
* 用户昵称
|
||||||
|
*/
|
||||||
|
nickname: string;
|
||||||
|
/**
|
||||||
|
* 用户头像地址
|
||||||
|
*/
|
||||||
|
avatar?: string;
|
||||||
|
/**
|
||||||
|
* JWT令牌
|
||||||
|
*/
|
||||||
|
accessToken?: string;
|
||||||
|
/**
|
||||||
|
* 刷新令牌
|
||||||
|
*/
|
||||||
|
refreshToken?: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,30 +61,30 @@ declare module '@vue/runtime-core' {
|
||||||
ATree: typeof import('@arco-design/web-vue')['Tree']
|
ATree: typeof import('@arco-design/web-vue')['Tree']
|
||||||
AUpload: typeof import('@arco-design/web-vue')['Upload']
|
AUpload: typeof import('@arco-design/web-vue')['Upload']
|
||||||
BaseOption: typeof import('./../components/editor/components/BaseOption.vue')['default']
|
BaseOption: typeof import('./../components/editor/components/BaseOption.vue')['default']
|
||||||
Block: typeof import('./../components/editor/panel-main/components/block.vue')['default']
|
|
||||||
BreadCrumb: typeof import('./../components/breadcrumb/bread-crumb.vue')['default']
|
BreadCrumb: typeof import('./../components/breadcrumb/bread-crumb.vue')['default']
|
||||||
BreadPage: typeof import('./../components/breadcrumb/bread-page.vue')['default']
|
BreadPage: typeof import('./../components/breadcrumb/bread-page.vue')['default']
|
||||||
ColorPicker: typeof import('./../components/editor/components/ColorPicker.vue')['default']
|
ColorPicker: typeof import('./../components/editor/components/ColorPicker.vue')['default']
|
||||||
DragResizer: typeof import('./../components/editor/components/DragResizer.vue')['default']
|
DragResizer: typeof import('./../components/editor/components/DragResizer.vue')['default']
|
||||||
Editor: typeof import('./../components/editor/index.vue')['default']
|
Editor: typeof import('./../components/editor/components/Editor.vue')['default']
|
||||||
|
EditorPreview: typeof import('./../components/editor/components/EditorPreview.vue')['default']
|
||||||
Empty: typeof import('./../components/empty/index.vue')['default']
|
Empty: typeof import('./../components/empty/index.vue')['default']
|
||||||
Header: typeof import('./../components/editor/panel-main/components/header.vue')['default']
|
|
||||||
ImagePicker: typeof import('./../components/editor/components/ImagePicker.vue')['default']
|
ImagePicker: typeof import('./../components/editor/components/ImagePicker.vue')['default']
|
||||||
InputColor: typeof import('./../components/editor/components/InputColor.vue')['default']
|
InputColor: typeof import('./../components/editor/components/InputColor.vue')['default']
|
||||||
InputImage: typeof import('./../components/editor/components/InputImage.vue')['default']
|
InputImage: typeof import('./../components/editor/components/InputImage.vue')['default']
|
||||||
|
InputTexter: typeof import('./../components/editor/components/InputTexter.vue')['default']
|
||||||
Marquee: typeof import('./../components/editor/blocks/text/marquee.vue')['default']
|
Marquee: typeof import('./../components/editor/blocks/text/marquee.vue')['default']
|
||||||
Option: typeof import('./../components/editor/blocks/date/option.vue')['default']
|
Option: typeof import('./../components/editor/blocks/date/option.vue')['default']
|
||||||
Page403: typeof import('./../components/error/page-403.vue')['default']
|
Page403: typeof import('./../components/error/page-403.vue')['default']
|
||||||
PanelHeader: typeof import('./../components/editor/panel-header/index.vue')['default']
|
PanelHeader: typeof import('./../components/editor/components/PanelHeader.vue')['default']
|
||||||
PanelLeft: typeof import('./../components/editor/panel-left/index.vue')['default']
|
PanelLeft: typeof import('./../components/editor/components/PanelLeft.vue')['default']
|
||||||
PanelMain: typeof import('./../components/editor/panel-main/index.vue')['default']
|
PanelMain: typeof import('./../components/editor/components/PanelMain.vue')['default']
|
||||||
PanelRight: typeof import('./../components/editor/panel-right/index.vue')['default']
|
PanelMainBlock: typeof import('./../components/editor/components/PanelMainBlock.vue')['default']
|
||||||
Preview: typeof import('./../components/editor/preview/index.vue')['default']
|
PanelMainHeader: typeof import('./../components/editor/components/PanelMainHeader.vue')['default']
|
||||||
|
PanelRight: typeof import('./../components/editor/components/PanelRight.vue')['default']
|
||||||
Render: typeof import('./../components/editor/blocks/date/render.vue')['default']
|
Render: typeof import('./../components/editor/blocks/date/render.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
'Temp.dev1': typeof import('./../components/breadcrumb/temp.dev1.vue')['default']
|
'Temp.dev1': typeof import('./../components/breadcrumb/temp.dev1.vue')['default']
|
||||||
Texter: typeof import('./../components/editor/panel-main/components/texter.vue')['default']
|
|
||||||
Toast: typeof import('./../components/toast/toast.vue')['default']
|
Toast: typeof import('./../components/toast/toast.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import 'vite';
|
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
/**
|
/**
|
||||||
|
|
@ -25,6 +24,14 @@ interface ImportMetaEnv {
|
||||||
* 开发服务器端口
|
* 开发服务器端口
|
||||||
*/
|
*/
|
||||||
VITE_PORT: number;
|
VITE_PORT: number;
|
||||||
|
/**
|
||||||
|
* 首页路径
|
||||||
|
*/
|
||||||
|
VITE_HOME_PATH: string;
|
||||||
|
/**
|
||||||
|
* 路由模式
|
||||||
|
*/
|
||||||
|
VITE_HISTORY: "web" | "hash";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'vue-router';
|
import "vue-router";
|
||||||
|
|
||||||
declare module 'vue-router' {
|
declare module "vue-router" {
|
||||||
interface RouteRecordRaw {
|
interface RouteRecordSingleView {
|
||||||
parentMeta: {
|
parentMeta: {
|
||||||
/**
|
/**
|
||||||
* 页面标题
|
* 页面标题
|
||||||
|
|
@ -34,7 +34,7 @@ declare module 'vue-router' {
|
||||||
/**
|
/**
|
||||||
* 是否在菜单导航中隐藏
|
* 是否在菜单导航中隐藏
|
||||||
*/
|
*/
|
||||||
hidden?: boolean;
|
hide?: boolean;
|
||||||
/**
|
/**
|
||||||
* 所需权限
|
* 所需权限
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -33,3 +33,29 @@ export function treeEach(tree: any[], fn: (item: any) => void, before = true) {
|
||||||
!before && fn(item);
|
!before && fn(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找子项
|
||||||
|
* @param tree 树结构
|
||||||
|
* @param fn 函数
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function treeFind<T extends { children?: T[]; [key: string]: any } = any>(
|
||||||
|
tree: T[],
|
||||||
|
fn: (item: T) => boolean
|
||||||
|
): T | null {
|
||||||
|
let data: T | null = null;
|
||||||
|
for (const item of tree) {
|
||||||
|
if (fn(item)) {
|
||||||
|
data = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (item.children) {
|
||||||
|
data = treeFind(item.children, fn);
|
||||||
|
if (data) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue