feat: 优化编辑器逻辑
parent
7f9cbe8466
commit
4aef16583d
4
.env
4
.env
|
|
@ -2,9 +2,9 @@
|
|||
# 应用配置
|
||||
# =====================================================================================
|
||||
# 网站标题
|
||||
VITE_TITLE = 绝弹管理中心
|
||||
VITE_TITLE = 管理中心
|
||||
# 网站副标题
|
||||
VITE_SUBTITLE = 绝弹管理中心1
|
||||
VITE_SUBTITLE = 绝弹管理中心
|
||||
# 部署路径: 当为 ./ 时路由模式需为 hash
|
||||
VITE_BASE = /
|
||||
# 接口前缀:参见 axios 的 baseURL
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
- 遵循 Conventional Changelog 规范, 自动生成版本记录文档
|
||||
- 内置常用 VsCode 代码片段和推荐扩展,提升开发效率
|
||||
- 支持路由动态打包、路由权限、路由缓存和动态首页
|
||||
- 支持 Docker 部署,包含优化过的 Dockerfile 配置
|
||||
|
||||
## 快速开始
|
||||
|
||||
|
|
|
|||
80
index.html
80
index.html
|
|
@ -9,16 +9,13 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="app" class="dark:bg-slate-900 dark:text-slate-200">
|
||||
<div class="loading">
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHN0eWxlPSJtYXJnaW46IGF1dG87IGJhY2tncm91bmQ6IHJnYigyNTUsIDI1NSwgMjU1KTsgZGlzcGxheTogYmxvY2s7IHNoYXBlLXJlbmRlcmluZzogYXV0bzsiIHdpZHRoPSIyMDBweCIgaGVpZ2h0PSIyMDBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1MCA1MCkiPjxnPjxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiB2YWx1ZXM9IjA7NDUiIGtleVRpbWVzPSIwOzEiIGR1cj0iMC4ycyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiPjwvYW5pbWF0ZVRyYW5zZm9ybT48cGF0aCBkPSJNMjkuNDkxNTI0MjA2MTE3MjU1IC01LjUgTDM3LjQ5MTUyNDIwNjExNzI1NSAtNS41IEwzNy40OTE1MjQyMDYxMTcyNTUgNS41IEwyOS40OTE1MjQyMDYxMTcyNTUgNS41IEEzMCAzMCAwIDAgMSAyNC43NDI3NDQwNTAxOTg3MzggMTYuOTY0NTY5NDU3MTQ2NzEyIEwyNC43NDI3NDQwNTAxOTg3MzggMTYuOTY0NTY5NDU3MTQ2NzEyIEwzMC4zOTk1OTgyOTk2OTExMTcgMjIuNjIxNDIzNzA2NjM5MDkyIEwyMi42MjE0MjM3MDY2MzkwOTYgMzAuMzk5NTk4Mjk5NjkxMTE0IEwxNi45NjQ1Njk0NTcxNDY3MTYgMjQuNzQyNzQ0MDUwMTk4NzM0IEEzMCAzMCAwIDAgMSA1LjUgMjkuNDkxNTI0MjA2MTE3MjU1IEw1LjUgMjkuNDkxNTI0MjA2MTE3MjU1IEw1LjUgMzcuNDkxNTI0MjA2MTE3MjU1IEwtNS40OTk5OTk5OTk5OTk5OTcgMzcuNDkxNTI0MjA2MTE3MjU1IEwtNS40OTk5OTk5OTk5OTk5OTcgMjkuNDkxNTI0MjA2MTE3MjU1IEEzMCAzMCAwIDAgMSAtMTYuOTY0NTY5NDU3MTQ2NzA1IDI0Ljc0Mjc0NDA1MDE5ODczOCBMLTE2Ljk2NDU2OTQ1NzE0NjcwNSAyNC43NDI3NDQwNTAxOTg3MzggTC0yMi42MjE0MjM3MDY2MzkwODUgMzAuMzk5NTk4Mjk5NjkxMTE3IEwtMzAuMzk5NTk4Mjk5NjkxMTE3IDIyLjYyMTQyMzcwNjYzOTA5MiBMLTI0Ljc0Mjc0NDA1MDE5ODczOCAxNi45NjQ1Njk0NTcxNDY3MTIgQTMwIDMwIDAgMCAxIC0yOS40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMDkgTC0yOS40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMDkgTC0zNy40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMSBMLTM3LjQ5MTUyNDIwNjExNzI1NSAtNS41MDAwMDAwMDAwMDAwMDEgTC0yOS40OTE1MjQyMDYxMTcyNTUgLTUuNTAwMDAwMDAwMDAwMDAyIEEzMCAzMCAwIDAgMSAtMjQuNzQyNzQ0MDUwMTk4NzM4IC0xNi45NjQ1Njk0NTcxNDY3MDUgTC0yNC43NDI3NDQwNTAxOTg3MzggLTE2Ljk2NDU2OTQ1NzE0NjcwNSBMLTMwLjM5OTU5ODI5OTY5MTExNyAtMjIuNjIxNDIzNzA2NjM5MDg1IEwtMjIuNjIxNDIzNzA2NjM5MDkyIC0zMC4zOTk1OTgyOTk2OTExMTcgTC0xNi45NjQ1Njk0NTcxNDY3MTIgLTI0Ljc0Mjc0NDA1MDE5ODczOCBBMzAgMzAgMCAwIDEgLTUuNTAwMDAwMDAwMDAwMDExIC0yOS40OTE1MjQyMDYxMTcyNTUgTC01LjUwMDAwMDAwMDAwMDAxMSAtMjkuNDkxNTI0MjA2MTE3MjU1IEwtNS41MDAwMDAwMDAwMDAwMTIgLTM3LjQ5MTUyNDIwNjExNzI1NSBMNS40OTk5OTk5OTk5OTk5OTggLTM3LjQ5MTUyNDIwNjExNzI1NSBMNS41IC0yOS40OTE1MjQyMDYxMTcyNTUgQTMwIDMwIDAgMCAxIDE2Ljk2NDU2OTQ1NzE0NjcwMiAtMjQuNzQyNzQ0MDUwMTk4NzQgTDE2Ljk2NDU2OTQ1NzE0NjcwMiAtMjQuNzQyNzQ0MDUwMTk4NzQgTDIyLjYyMTQyMzcwNjYzOTA4IC0zMC4zOTk1OTgyOTk2OTExMiBMMzAuMzk5NTk4Mjk5NjkxMTE3IC0yMi42MjE0MjM3MDY2MzkxIEwyNC43NDI3NDQwNTAxOTg3MzggLTE2Ljk2NDU2OTQ1NzE0NjcxNiBBMzAgMzAgMCAwIDEgMjkuNDkxNTI0MjA2MTE3MjU1IC01LjUwMDAwMDAwMDAwMDAxMyBNMCAtMjBBMjAgMjAgMCAxIDAgMCAyMCBBMjAgMjAgMCAxIDAgMCAtMjAiIGZpbGw9IiMwOWYiPjwvcGF0aD48L2c+PC9nPjwvc3ZnPgo="
|
||||
alt="loading"
|
||||
class="loading-image"
|
||||
width="64"
|
||||
height="64"
|
||||
/>
|
||||
<h1 class="loading-title">欢迎访问%VITE_TITLE%</h1>
|
||||
<div class="loading-tip">资源加载中, 请稍等...</div>
|
||||
<div class="cube">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<style>
|
||||
html,
|
||||
|
|
@ -29,8 +26,7 @@
|
|||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
font-family: Inter, '-apple-system', BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'noto sans',
|
||||
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-family: Inter, '-apple-system', BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'noto sans', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
#app {
|
||||
width: 100%;
|
||||
|
|
@ -40,27 +36,51 @@
|
|||
justify-content: center;
|
||||
user-select: none;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@keyframes cube {
|
||||
0% {
|
||||
transform: rotate(45deg) rotateX(-25deg) rotateY(25deg);
|
||||
}
|
||||
.loading-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
50% {
|
||||
transform: rotate(45deg) rotateX(-385deg) rotateY(25deg);
|
||||
}
|
||||
.loading-title {
|
||||
margin: 0;
|
||||
margin-top: 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
100% {
|
||||
transform: rotate(45deg) rotateX(-385deg) rotateY(385deg);
|
||||
}
|
||||
.loading-tip {
|
||||
margin-top: 12px;
|
||||
line-height: 1;
|
||||
color: #889;
|
||||
}
|
||||
.cube {
|
||||
animation: cube 2s infinite ease;
|
||||
height: 40px;
|
||||
transform-style: preserve-3d;
|
||||
width: 40px;
|
||||
}
|
||||
.cube div {
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
border: 2px solid #000;
|
||||
}
|
||||
.cube div:nth-of-type(1) {
|
||||
transform: translateZ(-20px) rotateY(180deg);
|
||||
}
|
||||
.cube div:nth-of-type(2) {
|
||||
transform: rotateY(-270deg) translateX(50%);
|
||||
transform-origin: top right;
|
||||
}
|
||||
.cube div:nth-of-type(3) {
|
||||
transform: rotateY(270deg) translateX(-50%);
|
||||
transform-origin: center left;
|
||||
}
|
||||
.cube div:nth-of-type(4) {
|
||||
transform: rotateX(90deg) translateY(-50%);
|
||||
transform-origin: top center;
|
||||
}
|
||||
.cube div:nth-of-type(5) {
|
||||
transform: rotateX(-90deg) translateY(50%);
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
.cube div:nth-of-type(6) {
|
||||
transform: translateZ(20px);
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: rgb(255, 255, 255); display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><g transform="translate(50 50)"><g><animateTransform attributeName="transform" type="rotate" values="0;45" keyTimes="0;1" dur="0.2s" repeatCount="indefinite"></animateTransform><path d="M29.491524206117255 -5.5 L37.491524206117255 -5.5 L37.491524206117255 5.5 L29.491524206117255 5.5 A30 30 0 0 1 24.742744050198738 16.964569457146712 L24.742744050198738 16.964569457146712 L30.399598299691117 22.621423706639092 L22.621423706639096 30.399598299691114 L16.964569457146716 24.742744050198734 A30 30 0 0 1 5.5 29.491524206117255 L5.5 29.491524206117255 L5.5 37.491524206117255 L-5.499999999999997 37.491524206117255 L-5.499999999999997 29.491524206117255 A30 30 0 0 1 -16.964569457146705 24.742744050198738 L-16.964569457146705 24.742744050198738 L-22.621423706639085 30.399598299691117 L-30.399598299691117 22.621423706639092 L-24.742744050198738 16.964569457146712 A30 30 0 0 1 -29.491524206117255 5.500000000000009 L-29.491524206117255 5.500000000000009 L-37.491524206117255 5.50000000000001 L-37.491524206117255 -5.500000000000001 L-29.491524206117255 -5.500000000000002 A30 30 0 0 1 -24.742744050198738 -16.964569457146705 L-24.742744050198738 -16.964569457146705 L-30.399598299691117 -22.621423706639085 L-22.621423706639092 -30.399598299691117 L-16.964569457146712 -24.742744050198738 A30 30 0 0 1 -5.500000000000011 -29.491524206117255 L-5.500000000000011 -29.491524206117255 L-5.500000000000012 -37.491524206117255 L5.499999999999998 -37.491524206117255 L5.5 -29.491524206117255 A30 30 0 0 1 16.964569457146702 -24.74274405019874 L16.964569457146702 -24.74274405019874 L22.62142370663908 -30.39959829969112 L30.399598299691117 -22.6214237066391 L24.742744050198738 -16.964569457146716 A30 30 0 0 1 29.491524206117255 -5.500000000000013 M0 -20A20 20 0 1 0 0 20 A20 20 0 1 0 0 -20" fill="#09f"></path></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
|
@ -1,5 +1,5 @@
|
|||
import { spawn } from 'child_process';
|
||||
import { Plugin, ResolvedConfig } from 'vite';
|
||||
import { Plugin } from 'vite';
|
||||
import pkg from '../../package.json';
|
||||
|
||||
/**
|
||||
|
|
@ -46,8 +46,11 @@ const getBuildInfo = async () => {
|
|||
const version = commits ? `${latestTag}.${commits}` : `v${pkg.version}`;
|
||||
const content = `欢迎访问!版本: ${version} 标识: ${hash} 构建: ${time}`;
|
||||
const style = `"color: #09f; font-weight: 900;", "font-size: 12px; color: #09f; font-family: ''"`;
|
||||
const script = `console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`;
|
||||
return script;
|
||||
const vString = `var __APP_VERSION__ = '${version}';\n`;
|
||||
const hString = `var __APP_HASH__ = '${hash}';\n`;
|
||||
const dString = `var __APP_DATE__ = '${time}';\n`;
|
||||
const lString = `console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`;
|
||||
return vString + hString + dString + lString;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -55,16 +58,9 @@ const getBuildInfo = async () => {
|
|||
* @returns Plugin
|
||||
*/
|
||||
export default function plugin(): Plugin {
|
||||
let config: ResolvedConfig;
|
||||
|
||||
return {
|
||||
name: 'vite:info',
|
||||
enforce: 'pre',
|
||||
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
},
|
||||
|
||||
async transformIndexHtml() {
|
||||
const script = await getBuildInfo();
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ const userStore = useUserStore();
|
|||
const menuStore = useMenuStore();
|
||||
|
||||
const hasAuth = computed(() => {
|
||||
if (!route.name.startsWith('_')) {
|
||||
return true;
|
||||
}
|
||||
return route.matched.every(item => {
|
||||
const needAuth = item.meta.auth;
|
||||
const userAuth = userStore.auth;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,28 @@
|
|||
import { Service } from './service';
|
||||
import { addToastInterceptor } from '../interceptors/toast';
|
||||
import { addAuthInterceptor } from '../interceptors/auth';
|
||||
import { addExceptionInterceptor } from '../interceptors/exception';
|
||||
import { env } from '@/config/env';
|
||||
import { App } from 'vue';
|
||||
import { Api } from '../generated/Api';
|
||||
|
||||
/**
|
||||
* 扩展生成的API类
|
||||
*/
|
||||
export class Service extends Api<unknown> {
|
||||
/**
|
||||
* 作为VUE插件进行初始化
|
||||
* @param app
|
||||
*/
|
||||
install(app: App) {
|
||||
app.config.globalProperties.$api = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登陆过期处理函数
|
||||
* @description 勿动
|
||||
*/
|
||||
expireHandler: () => void = () => {};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 接口实例
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import { App } from 'vue';
|
||||
import { Api } from '../generated/Api';
|
||||
|
||||
/**
|
||||
* 扩展生成的API类
|
||||
*/
|
||||
export class Service extends Api<unknown> {
|
||||
/**
|
||||
* 登陆过期处理函数
|
||||
* @description 勿动
|
||||
*/
|
||||
expireHandler: () => void = () => {};
|
||||
|
||||
/**
|
||||
* 作为VUE插件进行初始化
|
||||
* @param app
|
||||
*/
|
||||
install(app: App) {
|
||||
app.config.globalProperties.$api = this;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { InjectionKey } from 'vue';
|
||||
import { Block, Blocker, Container } from '../core';
|
||||
import { useTextBlock } from './text';
|
||||
|
||||
const blockers: Record<string, Blocker> = import.meta.glob(['./*/index.ts', '!./font/*'], {
|
||||
eager: true,
|
||||
|
|
@ -25,47 +24,3 @@ const getIcon = (type: string) => {
|
|||
};
|
||||
|
||||
export { BlockerMap, getBlockerRender, getIcon, getTypeName };
|
||||
|
||||
export const BlockerManagerKey = Symbol('k') as InjectionKey<ReturnType<typeof useBlockerManage>>
|
||||
|
||||
export function useBlockerManage() {
|
||||
const blockers: Blocker[] = [useTextBlock()];
|
||||
const leftPanels: any[] = [];
|
||||
|
||||
for (const blocker of blockers) {
|
||||
const panel = blocker.addLeftTab?.();
|
||||
if (panel) {
|
||||
leftPanels.push(leftPanels);
|
||||
}
|
||||
}
|
||||
|
||||
const callInitHook = (container: Container) => {
|
||||
for (const blocker of blockers) {
|
||||
container = blocker.onLoadContainer?.(container) || container;
|
||||
}
|
||||
return container;
|
||||
};
|
||||
|
||||
const callLoadHook = (data: any): Blocker => {
|
||||
for (const blocker of blockers) {
|
||||
data = blocker.onLoadBlock?.(data) || data;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const callSaveHook = (block: Block) => {
|
||||
let data = block;
|
||||
for (const blocker of blockers) {
|
||||
data = blocker.onSaveBlock?.(data) || data;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
return {
|
||||
blockers,
|
||||
leftPanels,
|
||||
callInitHook,
|
||||
callLoadHook,
|
||||
callSaveHook,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
import { Block, Blocker, defineBlocker } from '../../core';
|
||||
import { font } from '../font';
|
||||
import { Text } from './interface';
|
||||
import Option from './option.vue';
|
||||
import Render from './render.vue';
|
||||
|
||||
export default defineBlocker<Text>({
|
||||
type: 'text',
|
||||
icon: 'icon-park-outline-text',
|
||||
title: '文本组件',
|
||||
description: '文字',
|
||||
render: Render,
|
||||
option: Option,
|
||||
initial: {
|
||||
id: '',
|
||||
type: 'text',
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 100,
|
||||
xFixed: false,
|
||||
yFixed: false,
|
||||
bgImage: '',
|
||||
bgColor: '',
|
||||
meta: {},
|
||||
actived: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
params: {
|
||||
marquee: false,
|
||||
speed: 100,
|
||||
direction: 'left',
|
||||
fontCh: {
|
||||
...font,
|
||||
content:
|
||||
'温馨提示:乘客您好,进站检票时,持票卡的乘客请在右侧闸机上方感应区内验票,扫码过闸的乘客请将乘车码对准闸机扫码口,扇门打开后依次进闸。乘车过程中请妥善保管好车票,以免丢失。',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function useTextBlock(): Blocker<Text> {
|
||||
const initialData: Text = {
|
||||
id: '',
|
||||
type: 'text',
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 100,
|
||||
xFixed: false,
|
||||
yFixed: false,
|
||||
bgImage: '',
|
||||
bgColor: '',
|
||||
meta: {},
|
||||
actived: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
params: {
|
||||
marquee: false,
|
||||
speed: 100,
|
||||
direction: 'left',
|
||||
fontCh: {
|
||||
...font,
|
||||
content:
|
||||
'温馨提示:乘客您好,进站检票时,持票卡的乘客请在右侧闸机上方感应区内验票,扫码过闸的乘客请将乘车码对准闸机扫码口,扇门打开后依次进闸。乘车过程中请妥善保管好车票,以免丢失。',
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
type: 'text',
|
||||
icon: 'icon-park-outline-text',
|
||||
title: '文本组件',
|
||||
description: '文字',
|
||||
render: Render,
|
||||
option: Option,
|
||||
initial: initialData,
|
||||
addLeftTab() {
|
||||
return {
|
||||
title: '文本测试',
|
||||
icon: 'icon-park-outline-user',
|
||||
component: () => h('div', null, 'TODO')
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import { merge } from 'lodash-es';
|
||||
import { Block, Blocker, defineBlocker } from '../../core';
|
||||
import { BlockItem, Plugin } from '../../core/plugin';
|
||||
import { font } from '../font';
|
||||
import { Text } from './interface';
|
||||
import Option from './option.vue';
|
||||
import Render from './render.vue';
|
||||
import { Button } from '@arco-design/web-vue';
|
||||
|
||||
export default defineBlocker<Text>({
|
||||
type: 'text',
|
||||
icon: 'icon-park-outline-text',
|
||||
title: '文本组件',
|
||||
description: '文字',
|
||||
render: Render,
|
||||
option: Option,
|
||||
initial: {
|
||||
id: '',
|
||||
type: 'text',
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 100,
|
||||
xFixed: false,
|
||||
yFixed: false,
|
||||
bgImage: '',
|
||||
bgColor: '',
|
||||
meta: {},
|
||||
actived: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
params: {
|
||||
marquee: false,
|
||||
speed: 100,
|
||||
direction: 'left',
|
||||
fontCh: {
|
||||
...font,
|
||||
content:
|
||||
'温馨提示:乘客您好,进站检票时,持票卡的乘客请在右侧闸机上方感应区内验票,扫码过闸的乘客请将乘车码对准闸机扫码口,扇门打开后依次进闸。乘车过程中请妥善保管好车票,以免丢失。',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const defaults: Text = {
|
||||
id: '',
|
||||
type: 'text',
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 100,
|
||||
xFixed: false,
|
||||
yFixed: false,
|
||||
bgImage: '',
|
||||
bgColor: '',
|
||||
meta: {},
|
||||
actived: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
params: {
|
||||
marquee: false,
|
||||
speed: 100,
|
||||
direction: 'left',
|
||||
fontCh: {
|
||||
...font,
|
||||
content: '温馨提示:乘客您好',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const item: BlockItem = {
|
||||
type: 'text',
|
||||
icon: 'icon-park-outline-text',
|
||||
title: '文本组件',
|
||||
description: '文字',
|
||||
editRender: Option,
|
||||
viewRender: Render,
|
||||
onInit: () => {
|
||||
return merge({}, defaults);
|
||||
},
|
||||
};
|
||||
|
||||
export function TextBlock(): Plugin {
|
||||
const defaults = {
|
||||
id: '',
|
||||
type: 'text',
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 100,
|
||||
xFixed: false,
|
||||
yFixed: false,
|
||||
bgImage: '',
|
||||
bgColor: '',
|
||||
meta: {},
|
||||
actived: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
params: {
|
||||
marquee: false,
|
||||
speed: 100,
|
||||
direction: 'left',
|
||||
fontCh: {
|
||||
...font,
|
||||
content: '温馨提示:乘客您好',
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
name: 'TextBlockPlugin',
|
||||
hrRender: {
|
||||
name: 'TextDelete',
|
||||
render() {
|
||||
return (
|
||||
<Button>
|
||||
{{
|
||||
icon: <i class="icon-park-outline-delete"></i>,
|
||||
default: '测试',
|
||||
}}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
hlRender: {
|
||||
name: 'tip',
|
||||
render() {
|
||||
return <span class="text-gray-400 text-xs ml-2">测试提示</span>;
|
||||
},
|
||||
},
|
||||
addBlockItem() {
|
||||
return {
|
||||
type: 'text',
|
||||
icon: 'icon-park-outline-text',
|
||||
title: '文本组件',
|
||||
description: '文字',
|
||||
onInit: () => merge({}, defaults),
|
||||
editRender: Option,
|
||||
viewRender: Render,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -2,41 +2,23 @@
|
|||
<a-modal v-model:visible="show" :fullscreen="true" :footer="false" class="an-editor">
|
||||
<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">
|
||||
<EditorHeader
|
||||
v-model:container="container"
|
||||
:saving="saving"
|
||||
@preview="showPreview = true"
|
||||
@config="showConfig = true"
|
||||
@exit="onExit()"
|
||||
@save="saveData()"
|
||||
></EditorHeader>
|
||||
<EditorHeader v-model:container="container" :saving="saving" @preview="showPreview = true" @config="showConfig = true" @exit="onExit()" @save="saveData()"></EditorHeader>
|
||||
</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">
|
||||
<EditorLeft @rm-block="rmBlock" @current-block="setCurrentBlock"></EditorLeft>
|
||||
</div>
|
||||
<div class="w-full h-full">
|
||||
<EditorMain
|
||||
v-model:rightPanelCollapsed="rightPanelCollapsed"
|
||||
@add-block="addBlock"
|
||||
@current-block="setCurrentBlock"
|
||||
@block-menu="onBlockContextMenu"
|
||||
></EditorMain>
|
||||
<EditorMain v-model:rightPanelCollapsed="rightPanelCollapsed" @add-block="addBlock" @current-block="setCurrentBlock" @block-menu="onBlockContextMenu"></EditorMain>
|
||||
</div>
|
||||
<div class="h-full overflow-hidden bg-white shadow-[-2px_0_6px_rgba(0,0,0,.05)]">
|
||||
<EditorRight v-model:collapsed="rightPanelCollapsed" v-model:block="currentBlock"></EditorRight>
|
||||
<EditorRight v-model:collapsed="rightPanelCollapsed" v-model:block="container.current"></EditorRight>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EditorPreview v-model:visible="showPreview" :container="container" :blocks="blocks"></EditorPreview>
|
||||
<EditorPreview v-model:visible="showPreview" :container="container" :blocks="container.children"></EditorPreview>
|
||||
<EditorSetting v-model:visible="showConfig" v-model="container"></EditorSetting>
|
||||
<ContextMenu
|
||||
v-model:visible="blockMenu.show"
|
||||
:x="blockMenu.x"
|
||||
:y="blockMenu.y"
|
||||
:items="blockMenuItems"
|
||||
@done="blockMenu.show = false"
|
||||
></ContextMenu>
|
||||
<ContextMenu v-model:visible="blockMenu.show" :x="blockMenu.x" :y="blockMenu.y" :items="blockMenuItems" @done="blockMenu.show = false"></ContextMenu>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
|
|
@ -44,7 +26,7 @@
|
|||
import { delConfirm, sleep } from '@/utils';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Block, ContextMenuItem, EditorKey, useEditor } from '../core';
|
||||
import { Block, EditorKey, useEditor } from '../core';
|
||||
import ContextMenu from './ContextMenu.vue';
|
||||
import EditorSetting from './EditorConfig.vue';
|
||||
import EditorHeader from './EditorHeader.vue';
|
||||
|
|
@ -52,6 +34,8 @@ import EditorLeft from './EditorLeft.vue';
|
|||
import EditorMain from './EditorMain.vue';
|
||||
import EditorPreview from './EditorPreview.vue';
|
||||
import EditorRight from './EditorRight.vue';
|
||||
import { ContextKey, usePluginContext } from '../core/plugin';
|
||||
import { TextBlock } from '../blocks/text';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
|
|
@ -67,7 +51,10 @@ const showPreview = ref(false);
|
|||
const showConfig = ref(false);
|
||||
const saving = ref(false);
|
||||
const editor = useEditor();
|
||||
const { container, blocks, currentBlock, addBlock, rmBlock, setCurrentBlock } = editor;
|
||||
const context = usePluginContext([TextBlock()]);
|
||||
const { container, addBlock, rmBlock, setCurrentBlock } = context;
|
||||
|
||||
console.log({context});
|
||||
|
||||
const blockMenu = reactive<{ show: boolean; x: number; y: number; block: Block | null }>({
|
||||
show: false,
|
||||
|
|
@ -76,7 +63,7 @@ const blockMenu = reactive<{ show: boolean; x: number; y: number; block: Block |
|
|||
block: null,
|
||||
});
|
||||
|
||||
const blockMenuItems: ContextMenuItem[] = [
|
||||
const blockMenuItems: any[] = [
|
||||
{
|
||||
name: '删除',
|
||||
icon: 'icon-park-outline-delete',
|
||||
|
|
@ -103,7 +90,7 @@ const onBlockContextMenu = (block: Block, e: MouseEvent) => {
|
|||
const saveData = async () => {
|
||||
const data = {
|
||||
container: container.value,
|
||||
children: blocks.value,
|
||||
children: container.value.children,
|
||||
};
|
||||
saving.value = true;
|
||||
await sleep(3000);
|
||||
|
|
@ -120,7 +107,7 @@ const loadData = async () => {
|
|||
}
|
||||
const data = JSON.parse(str);
|
||||
container.value = data.container;
|
||||
blocks.value = data.children;
|
||||
container.value.children = data.children;
|
||||
};
|
||||
|
||||
const onExit = async () => {
|
||||
|
|
@ -133,6 +120,7 @@ const onExit = async () => {
|
|||
};
|
||||
|
||||
provide(EditorKey, editor);
|
||||
provide(ContextKey, context);
|
||||
onMounted(loadData);
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,7 @@ const props = defineProps({
|
|||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'update:modelValue']);
|
||||
|
||||
const show = useVModel(props, 'visible', emit);
|
||||
const model = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@
|
|||
</a-link>
|
||||
<a-divider :direction="'vertical'" :margin="8"></a-divider>
|
||||
<ani-texter v-model="container.title"></ani-texter>
|
||||
<component v-for="item in HL" :is="item" />
|
||||
<!-- <a-tag :color="container.id ? 'blue' : 'green'" class="mr-2 ml-1">
|
||||
{{ container.id ? '修改' : '新增' }}
|
||||
</a-tag> -->
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<component v-for="item in HR" :key="item.name" :is="item" />
|
||||
<a-button @click="emit('preview')">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-play"></i>
|
||||
|
|
@ -38,6 +40,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { Container } from '../core';
|
||||
import { ContextKey } from '../core/plugin';
|
||||
import AniTexter from './InputTexter.vue';
|
||||
|
||||
defineProps({
|
||||
|
|
@ -48,8 +51,9 @@ defineProps({
|
|||
})
|
||||
|
||||
const emit = defineEmits(['preview', 'config', 'exit', 'save']);
|
||||
|
||||
const container = defineModel<Container>('container', { required: true });
|
||||
|
||||
const { HR, HL } = inject(ContextKey)!
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
<template>
|
||||
<div class="h-full grid grid-cols-[auto_1fr]" :style="{ width: !collapsed ? '248px' : undefined }">
|
||||
<div class="h-full grid grid-rows-[1fr_auto] border-r border-slate-200">
|
||||
<a-menu
|
||||
:collapsed="true"
|
||||
:default-selected-keys="['0_0']"
|
||||
:selected-keys="[key]"
|
||||
@menu-item-click="(k) => (key = k)"
|
||||
>
|
||||
<a-menu :collapsed="true" :default-selected-keys="['0_0']" :selected-keys="[key]" @menu-item-click="k => (key = k)">
|
||||
<a-menu-item key="list">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-all-application"></i>
|
||||
|
|
@ -31,10 +26,7 @@
|
|||
<a-tooltip :content="collapsed ? '展开' : '折叠'" position="right">
|
||||
<a-button type="text" @click="collapsed = !collapsed">
|
||||
<template #icon>
|
||||
<i
|
||||
class="text-lg text-gray-400 hover:text-gray-700"
|
||||
:class="collapsed ? 'icon-park-outline-expand-left' : 'icon-park-outline-expand-right'"
|
||||
></i>
|
||||
<i class="text-lg text-gray-400 hover:text-gray-700" :class="collapsed ? 'icon-park-outline-expand-left' : 'icon-park-outline-expand-right'"></i>
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
|
@ -61,13 +53,13 @@
|
|||
</ul>
|
||||
<ul v-show="key === 'data'" class="list-none px-2 grid gap-2">
|
||||
<li
|
||||
v-for="item in blocks"
|
||||
v-for="item in container.children"
|
||||
: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="{
|
||||
'!bg-brand-50': currentBlock === item,
|
||||
'!text-brand-500': currentBlock === item,
|
||||
'!border-brand-300': currentBlock === item,
|
||||
'!bg-brand-50': container.current === item,
|
||||
'!text-brand-500': container.current === item,
|
||||
'!border-brand-300': container.current === item,
|
||||
}"
|
||||
@click="emit('current-block', item)"
|
||||
>
|
||||
|
|
@ -78,10 +70,7 @@
|
|||
{{ item.title }}
|
||||
</div>
|
||||
<div class="w-4">
|
||||
<i
|
||||
class="!hidden !group-hover:inline-block text-gray-400 hover:text-gray-700 icon-park-outline-delete !text-xs"
|
||||
@click.prevent="emit('rm-block', item)"
|
||||
></i>
|
||||
<i class="!hidden !group-hover:inline-block text-gray-400 hover:text-gray-700 icon-park-outline-delete !text-xs" @click.prevent="emit('rm-block', item)"></i>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -90,24 +79,24 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIcon } from "../blocks";
|
||||
import { Block, EditorKey } from "../core";
|
||||
import { getIcon } from '../blocks';
|
||||
import { Block, EditorKey } from '../core';
|
||||
|
||||
const { blocks, currentBlock, BlockerMap } = inject(EditorKey)!;
|
||||
const { container, BlockerMap } = inject(EditorKey)!;
|
||||
const blockList = Object.values(BlockerMap);
|
||||
const collapsed = ref(false);
|
||||
const key = ref<"list" | "data">("list");
|
||||
const key = ref<'list' | 'data'>('list');
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "rm-block", block: Block): void;
|
||||
(event: "current-block", block: Block | null): void;
|
||||
(event: 'rm-block', block: Block): void;
|
||||
(event: 'current-block', block: Block | null): void;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* 拖拽开始时设置数据
|
||||
*/
|
||||
const onDragStart = (e: DragEvent) => {
|
||||
e.dataTransfer?.setData("type", (e.target as HTMLElement).dataset.type!);
|
||||
e.dataTransfer?.setData('type', (e.target as HTMLElement).dataset.type!);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,13 @@
|
|||
<template>
|
||||
<div class="h-full grid grid-rows-[auto_1fr]">
|
||||
<div class="h-10">
|
||||
<EditorMainHeader
|
||||
:container="container"
|
||||
v-model:rightPanelCollapsed="rightPanelCollapsed"
|
||||
@preview="emit('preview')"
|
||||
></EditorMainHeader>
|
||||
<EditorMainHeader :container="container" v-model:rightPanelCollapsed="rightPanelCollapsed" @preview="emit('preview')"></EditorMainHeader>
|
||||
</div>
|
||||
<div class="h-full w-full overflow-hidden p-4">
|
||||
<div
|
||||
class="juetan-editor-container w-full h-full flex items-center justify-center overflow-hidden relative bg-slate-50"
|
||||
>
|
||||
<div
|
||||
class="relative"
|
||||
:style="containerStyle"
|
||||
@dragover.prevent
|
||||
@click="onClick"
|
||||
@drop="onDragDrop"
|
||||
@wheel="onMouseWheel"
|
||||
@mousedown="onMouseDown"
|
||||
>
|
||||
<div class="juetan-editor-container w-full h-full flex items-center justify-center overflow-hidden relative bg-slate-50">
|
||||
<div class="relative" :style="containerStyle" @dragover.prevent @click="onClick" @drop="onDragDrop" @wheel="onMouseWheel" @mousedown="onMouseDown">
|
||||
<EditorMainBlock
|
||||
v-for="block in blocks"
|
||||
v-for="block in container.children"
|
||||
:key="block.id"
|
||||
:data="block"
|
||||
:container="container"
|
||||
|
|
@ -60,12 +46,13 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Block, EditorKey } from '../core';
|
||||
import { Block, EditorKey, formatContainerStyle } from '../core';
|
||||
import { ContextKey } from '../core/plugin';
|
||||
import EditorMainBlock from './EditorMainBlock.vue';
|
||||
import EditorMainHeader from './EditorMainHeader.vue';
|
||||
|
||||
const rightPanelCollapsed = defineModel<boolean>('rightPanelCollapsed');
|
||||
const { blocks, container, refLine, formatContainerStyle, scene } = inject(EditorKey)!;
|
||||
const { container, refLine, scene } = inject(ContextKey)!;
|
||||
const { onMouseDown, onMouseWheel } = scene;
|
||||
const { active, xLines, yLines } = refLine;
|
||||
|
||||
|
|
@ -107,14 +94,7 @@ const onDragDrop = (e: DragEvent) => {
|
|||
<style scoped>
|
||||
.juetan-editor-container {
|
||||
--color: rgba(0, 0, 0, 0.2);
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
var(--color) 25%,
|
||||
transparent 25%,
|
||||
transparent 75%,
|
||||
var(--color) 75%,
|
||||
var(--color) 100%
|
||||
),
|
||||
background: linear-gradient(45deg, var(--color) 25%, transparent 25%, transparent 75%, var(--color) 75%, var(--color) 100%),
|
||||
linear-gradient(45deg, var(--color) 25%, transparent 25%, transparent 75%, var(--color) 75%, var(--color) 100%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 10px 10px;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@
|
|||
import { PropType } from "vue";
|
||||
import { BlockerMap } from "../blocks";
|
||||
import DragResizer from "./DragResizer.vue";
|
||||
import { Block, Container, EditorKey } from "../core";
|
||||
import { Block, Container } from "../core";
|
||||
import { ContextKey } from "../core/plugin";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
|
|
@ -41,7 +42,7 @@ const props = defineProps({
|
|||
},
|
||||
});
|
||||
|
||||
const { setCurrentBlock, refLine } = inject(EditorKey)!;
|
||||
const { setCurrentBlock, refLine } = inject(ContextKey)!;
|
||||
const { active, recordBlocksXY, updateRefLine } = refLine;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
</span>
|
||||
<span class="text-gray-400 text-xs mr-2">
|
||||
组件:
|
||||
<span class="inline-block w-8 text-gray-700">{{ blocks.length }} 个</span>
|
||||
<span class="inline-block w-8 text-gray-700">{{ container.children.length }} 个</span>
|
||||
</span>
|
||||
<a-tooltip content="自适应比例" position="bottom">
|
||||
<a-button type="text" @click="setContainerOrigin">
|
||||
|
|
@ -62,8 +62,8 @@
|
|||
<script setup lang="ts">
|
||||
import InputTexter from './InputTexter.vue';
|
||||
// import EditorMainConfig from './EditorMainConfig.vue';
|
||||
import { EditorKey } from '../core';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { ContextKey } from '../core/plugin';
|
||||
|
||||
const props = defineProps({
|
||||
rightPanelCollapsed: {
|
||||
|
|
@ -74,7 +74,7 @@ const props = defineProps({
|
|||
|
||||
const emit = defineEmits(['preview', 'update:rightPanelCollapsed']);
|
||||
const collapsed = useVModel(props, 'rightPanelCollapsed', emit);
|
||||
const { container, blocks, setContainerOrigin } = inject(EditorKey)!;
|
||||
const { container, setContainerOrigin } = inject(ContextKey)!;
|
||||
|
||||
const visible = ref(false);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<template #icon>
|
||||
<i :class="BlockerMap[model.type].icon"></i>
|
||||
</template>
|
||||
{{ BlockerMap[model.type].title }}属性
|
||||
{{ BlockerMap[model.type].title }}
|
||||
</a-tag>
|
||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
||||
<a-form :model="{}" layout="vertical" class="pr-3">
|
||||
|
|
@ -23,12 +23,13 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { BlockerMap } from '../blocks';
|
||||
import { Block, EditorKey } from '../core';
|
||||
import { Block } from '../core';
|
||||
import { ContextKey } from '../core/plugin';
|
||||
import EditorSetting from './EditorSetting.vue';
|
||||
|
||||
const collapsed = defineModel<boolean>('collapsed');
|
||||
const model = defineModel<Block | null>('block');
|
||||
const { container } = inject(EditorKey)!;
|
||||
const { container } = inject(ContextKey)!;
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="p-3">
|
||||
<a-tag class="text-sm! mb-2 w-full" size="large" color="blue" :bordered="false">
|
||||
<a-tag class="text-sm! mb-2 w-full" size="large" color="red" :bordered="false">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-config" ></i>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component } from "vue";
|
||||
import { Component } from 'vue';
|
||||
|
||||
/**
|
||||
* 组件参数
|
||||
|
|
@ -70,25 +70,11 @@ export interface Block<T = any> {
|
|||
params: T;
|
||||
}
|
||||
|
||||
export interface ContextMenuItem {
|
||||
type?: 'divider' | 'menu'
|
||||
showChildren?: boolean
|
||||
onClick?: (item: ContextMenuItem) => void;
|
||||
icon?: Component | string
|
||||
name: string
|
||||
tip?: string
|
||||
class?: string;
|
||||
children?: ContextMenuItem[]
|
||||
}
|
||||
|
||||
export const useBlockContextMenu = (blocks: Block[]) => {
|
||||
const items: ContextMenuItem[] = [
|
||||
{
|
||||
name: '删除',
|
||||
icon: () => h('i', { class: 'icon-park-outline-delete' }),
|
||||
onClick(item) {
|
||||
|
||||
},
|
||||
}
|
||||
]
|
||||
export function formatBlockStyle(block: Block) {
|
||||
const { bgColor, bgImage } = block;
|
||||
return {
|
||||
backgroundColor: bgColor,
|
||||
backgroundImage: bgImage ? `url(${bgImage})` : null,
|
||||
backgroundSize: '100% 100%',
|
||||
};
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
import { CSSProperties } from 'vue';
|
||||
import { Block } from './block';
|
||||
|
||||
/**
|
||||
* 画布配置
|
||||
*/
|
||||
|
|
@ -42,8 +45,22 @@ export interface Container {
|
|||
* 背景颜色
|
||||
*/
|
||||
bgColor: string;
|
||||
/**
|
||||
* 使用的语言列表
|
||||
*/
|
||||
langList: string[];
|
||||
/**
|
||||
* 语言的切换间隔
|
||||
*/
|
||||
langSwitch: number;
|
||||
/**
|
||||
* 组件列表
|
||||
*/
|
||||
children: Block[];
|
||||
/**
|
||||
* 当前选中的组件
|
||||
*/
|
||||
current: Block | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -51,15 +68,30 @@ export interface Container {
|
|||
*/
|
||||
export const defaultContainer: Container = {
|
||||
id: 11,
|
||||
title: "国庆节喜庆版式设计",
|
||||
description: "适用于国庆节1日-7日间上午9:00-10:00播出的版式设计",
|
||||
title: '国庆节喜庆版式设计',
|
||||
description: '适用于国庆节1日-7日间上午9:00-10:00播出的版式设计',
|
||||
x: 0,
|
||||
y: 0,
|
||||
zoom: 0.7,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bgImage: "",
|
||||
bgColor: "#ffffff",
|
||||
bgImage: '',
|
||||
bgColor: '#ffffff',
|
||||
langList: ['ch', 'en'],
|
||||
langSwitch: 0
|
||||
langSwitch: 0,
|
||||
children: [],
|
||||
current: null,
|
||||
};
|
||||
|
||||
export function 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})` : null,
|
||||
backgroundSize: '100% 100%',
|
||||
transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
|
||||
} as CSSProperties;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,16 @@
|
|||
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";
|
||||
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[]>([]);
|
||||
/**
|
||||
* 选中组件
|
||||
*/
|
||||
|
|
@ -22,7 +18,7 @@ export const useEditor = () => {
|
|||
/**
|
||||
* 参考线
|
||||
*/
|
||||
const refLine = useReferenceLine(blocks, currentBlock);
|
||||
const refLine = useReferenceLine(container);
|
||||
/**
|
||||
* 画布移动和缩放
|
||||
*/
|
||||
|
|
@ -43,17 +39,11 @@ export const useEditor = () => {
|
|||
if (!blocker) {
|
||||
return;
|
||||
}
|
||||
const ids = blocks.value.map((i) => Number(i.id));
|
||||
const ids = container.value.children.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,
|
||||
});
|
||||
container.value.children.push({ ...cloneDeep(blocker.initial), id, x, y, title });
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -61,9 +51,9 @@ export const useEditor = () => {
|
|||
* @param block 组件
|
||||
*/
|
||||
const rmBlock = (block: Block) => {
|
||||
const index = blocks.value.indexOf(block);
|
||||
const index = container.value.children.indexOf(block);
|
||||
if (index > -1) {
|
||||
blocks.value.splice(index, 1);
|
||||
container.value.children.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -77,7 +67,7 @@ export const useEditor = () => {
|
|||
return {
|
||||
backgroundColor: bgColor,
|
||||
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
||||
backgroundSize: "100% 100%",
|
||||
backgroundSize: '100% 100%',
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -89,12 +79,12 @@ export const useEditor = () => {
|
|||
const formatContainerStyle = (container: Container) => {
|
||||
const { width, height, bgColor, bgImage, zoom, x, y } = container;
|
||||
return {
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
backgroundColor: bgColor,
|
||||
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
||||
backgroundSize: "100% 100%",
|
||||
backgroundSize: '100% 100%',
|
||||
transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
|
||||
} as CSSProperties;
|
||||
};
|
||||
|
|
@ -104,7 +94,7 @@ export const useEditor = () => {
|
|||
* @param block 组件
|
||||
*/
|
||||
const setCurrentBlock = (block: Block | null) => {
|
||||
for (const item of blocks.value) {
|
||||
for (const item of container.value.children) {
|
||||
item.actived = false;
|
||||
}
|
||||
if (!block) {
|
||||
|
|
@ -121,7 +111,7 @@ export const useEditor = () => {
|
|||
const setContainerOrigin = () => {
|
||||
container.value.x = 0;
|
||||
container.value.y = 0;
|
||||
const el = document.querySelector(".juetan-editor-container");
|
||||
const el = document.querySelector('.juetan-editor-container');
|
||||
if (el) {
|
||||
const { width, height } = el.getBoundingClientRect();
|
||||
const wZoom = width / container.value.width;
|
||||
|
|
@ -133,7 +123,6 @@ export const useEditor = () => {
|
|||
|
||||
return {
|
||||
container,
|
||||
blocks,
|
||||
currentBlock,
|
||||
refLine,
|
||||
scene,
|
||||
|
|
@ -147,4 +136,4 @@ export const useEditor = () => {
|
|||
};
|
||||
};
|
||||
|
||||
export const EditorKey = Symbol("EditorKey") as InjectionKey<ReturnType<typeof useEditor>>;
|
||||
export const EditorKey = Symbol('EditorKey') as InjectionKey<ReturnType<typeof useEditor>>;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
import { CSSProperties, Component } from 'vue';
|
||||
import { Block } from './block';
|
||||
import { Container, defaultContainer } from './container';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useReferenceLine } from './ref-line';
|
||||
import { useScene } from './scene';
|
||||
|
||||
export interface BlockItem {
|
||||
/**
|
||||
* 需唯一
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
icon: string;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* 在组件库时的渲染
|
||||
*/
|
||||
pickRender?: any;
|
||||
/**
|
||||
* 在列表中时的渲染
|
||||
*/
|
||||
listRender?: any;
|
||||
/**
|
||||
* 在编辑中时的渲染
|
||||
*/
|
||||
showRender?: any;
|
||||
/**
|
||||
* 在预览中时的渲染
|
||||
*/
|
||||
viewRender?: any;
|
||||
/**
|
||||
* 编辑属性时的渲染
|
||||
*/
|
||||
editRender?: any;
|
||||
/**
|
||||
* 初始化默认参数
|
||||
*/
|
||||
onInit?: any;
|
||||
/**
|
||||
* 转换数据
|
||||
*/
|
||||
onLoad?: any;
|
||||
/**
|
||||
* 转换数据
|
||||
*/
|
||||
onSave?: any;
|
||||
}
|
||||
|
||||
interface SortableRender {
|
||||
name: string;
|
||||
sort?: number;
|
||||
render: Component;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
/**
|
||||
* 名字需唯一
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
hlRender?: SortableRender;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
hrRender?: SortableRender;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
ltRender?: SortableRender;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
lbRender?: SortableRender;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
mlRender?: SortableRender;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
mrRender?: SortableRender;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
rtRender?: SortableRender;
|
||||
rbRender?: () => BlockItem;
|
||||
addBlockItem?: () => BlockItem;
|
||||
blockItems?: BlockItem | BlockItem[];
|
||||
onInit?: (context: any) => void;
|
||||
onSave?: () => void;
|
||||
onLoad?: (data: any) => void;
|
||||
}
|
||||
|
||||
export const usePluginContext = (pluginlist: Plugin[]) => {
|
||||
const container: Ref<Container> = ref(cloneDeep(defaultContainer)) as any;
|
||||
const blocks = computed(() => container.value.children);
|
||||
const blockerMap: Record<string, BlockItem> = {};
|
||||
const refLine = useReferenceLine(container);
|
||||
const scene = useScene(container);
|
||||
|
||||
/** 顶部栏左侧 */
|
||||
const HL: Ref<SortableRender[]> = ref([]);
|
||||
/** 顶部栏右侧 */
|
||||
const HR: Ref<SortableRender[]> = ref([]);
|
||||
/** 左侧栏顶部 */
|
||||
const LC: Ref<SortableRender[]> = ref([]);
|
||||
/** 左侧栏底部 */
|
||||
const LB: Ref<SortableRender[]> = ref([]);
|
||||
/** 中间栏左侧 */
|
||||
const ML: Ref<SortableRender[]> = ref([]);
|
||||
/** 中间栏右侧 */
|
||||
const MR: Ref<SortableRender[]> = ref([]);
|
||||
/** 右侧栏顶部 */
|
||||
const RC: Ref<SortableRender[]> = ref([]);
|
||||
|
||||
function load(data: any) {
|
||||
data.children = data.children.map(item => {
|
||||
return blockerMap[item.type]?.onLoad?.(item) ?? item;
|
||||
});
|
||||
for (const plugin of pluginlist) {
|
||||
data = plugin.onLoad?.(data) ?? data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function save(container: Container) {}
|
||||
|
||||
function addBlock(type: string, x = 0, y = 0) {
|
||||
const blocker = blockerMap[type];
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
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}`;
|
||||
const block = { ...cloneDeep(blocker.onInit?.()), id, x, y, title };
|
||||
blocks.value.push(block);
|
||||
}
|
||||
|
||||
function rmBlock(block: Block) {
|
||||
const index = blocks.value.indexOf(block);
|
||||
if (index > -1) {
|
||||
blocks.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentBlock(block: Block | null) {
|
||||
for (const item of container.value.children) {
|
||||
item.actived = false;
|
||||
}
|
||||
if (!block) {
|
||||
container.value.current = null;
|
||||
} else {
|
||||
block.actived = true;
|
||||
container.value.current = block;
|
||||
}
|
||||
}
|
||||
|
||||
function 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;
|
||||
}
|
||||
}
|
||||
|
||||
const context = {
|
||||
container,
|
||||
blockerMap,
|
||||
refLine,
|
||||
scene,
|
||||
HL,
|
||||
HR,
|
||||
LB,
|
||||
LC,
|
||||
ML,
|
||||
MR,
|
||||
RC,
|
||||
setCurrentBlock,
|
||||
setContainerOrigin,
|
||||
addBlock,
|
||||
rmBlock,
|
||||
};
|
||||
|
||||
function addRender(list: any[], render?: SortableRender) {
|
||||
if (!render) {
|
||||
return;
|
||||
}
|
||||
if (list.some(i => i.name === render.name)) {
|
||||
console.log('name has existed');
|
||||
return;
|
||||
}
|
||||
list.push(render);
|
||||
}
|
||||
|
||||
for (const plugin of pluginlist) {
|
||||
plugin.onInit?.(context);
|
||||
addRender(HL.value, plugin.hlRender);
|
||||
addRender(HR.value, plugin.hrRender);
|
||||
addRender(LC.value, plugin.ltRender);
|
||||
addRender(LB.value, plugin.lbRender);
|
||||
addRender(ML.value, plugin.mlRender);
|
||||
addRender(MR.value, plugin.mrRender);
|
||||
addRender(RC.value, plugin.rtRender);
|
||||
const bi = plugin.addBlockItem?.();
|
||||
if (bi) {
|
||||
blockerMap[bi.type] = bi;
|
||||
}
|
||||
}
|
||||
|
||||
HL.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
HR.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
LC.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
LB.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
ML.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
MR.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
RC.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const ContextKey = Symbol('ContextKey') as InjectionKey<ReturnType<typeof usePluginContext>>;
|
||||
|
||||
function corePlugin(): Plugin {
|
||||
return {
|
||||
name: 'core',
|
||||
rtRender: {
|
||||
name: 'ss',
|
||||
render() {
|
||||
return () => 123;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { Ref } from "vue";
|
||||
import { getClosestValInSortedArr } from "../utils/closest";
|
||||
import { Block } from "./block";
|
||||
import { Container } from "./container";
|
||||
|
||||
/**
|
||||
* 组件参考线
|
||||
|
|
@ -8,7 +9,7 @@ import { Block } from "./block";
|
|||
* @param current 当前组件
|
||||
* @returns
|
||||
*/
|
||||
export const useReferenceLine = (blocks: Ref<Block[]>, current: Ref<Block | null>) => {
|
||||
export const useReferenceLine = (container: Ref<Container>) => {
|
||||
let xYsMap = new Map<number, number[]>();
|
||||
let yXsMap = new Map<number, number[]>();
|
||||
let sortedXs: number[] = [];
|
||||
|
|
@ -22,8 +23,8 @@ export const useReferenceLine = (blocks: Ref<Block[]>, current: Ref<Block | null
|
|||
*/
|
||||
const recordBlocksXY = () => {
|
||||
clear();
|
||||
for (const block of blocks.value) {
|
||||
if (block === current.value) {
|
||||
for (const block of container.value.children) {
|
||||
if (block === container.value.current) {
|
||||
continue;
|
||||
}
|
||||
const { minX, minY, midX, midY, maxX, maxY } = getBlockBox(block);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
export function arraify<T>(data: T | T[]): T[] {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray(data) ? data : [data];
|
||||
}
|
||||
|
|
@ -4,6 +4,16 @@ import localData from 'dayjs/plugin/localeData';
|
|||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { App } from 'vue';
|
||||
|
||||
declare module 'dayjs' {
|
||||
export var DATETIME: 'YYYY-MM-DD HH:mm';
|
||||
export var DATE: 'YYYY-MM-DD';
|
||||
export var TIME: 'HH:mm:ss';
|
||||
export var install: (app: App) => void;
|
||||
interface Dayjs {
|
||||
_format: Dayjs['format'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文语言包
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import 'dayjs';
|
||||
import { App } from 'vue';
|
||||
|
||||
declare module 'dayjs' {
|
||||
/**
|
||||
* 默认日期时间格式
|
||||
*/
|
||||
export var DATETIME: 'YYYY-MM-DD HH:mm';
|
||||
|
||||
export var DATE: 'YYYY-MM-DD';
|
||||
|
||||
export var TIME: 'HH:mm:ss';
|
||||
|
||||
interface Dayjs {
|
||||
_format: Dayjs['format'];
|
||||
}
|
||||
|
||||
export var install: (app: App) => void;
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
</a-button> -->
|
||||
<router-link to="/" class="px-2 flex items-center gap-2 text-slate-700">
|
||||
<img src="/favicon.ico" alt="" width="24" height="24" class="" />
|
||||
<h1 class="relative text-[18px] leading-[22px] dark:text-white m-0 p-0 font-semibold">
|
||||
<h1 class="relative text-[18px] leading-[22px] dark:text-white m-0 p-0 font-normal">
|
||||
{{ appStore.title }}
|
||||
<span class="absolute -right-10 -top-1 font-normal text-xs text-gray-400"> v0.0.1 </span>
|
||||
</h1>
|
||||
|
|
@ -55,13 +55,14 @@
|
|||
</a-layout-sider>
|
||||
<a-layout class="layout-content flex-1">
|
||||
<a-layout-content class="overflow-x-auto">
|
||||
<a-spin :loading="appStore.pageLoding" tip="页面加载中,请稍等..." class="block h-full w-full">
|
||||
<a-spin :loading="appStore.pageLoding" class="block h-full w-full">
|
||||
<template #icon>
|
||||
<IconSync></IconSync>
|
||||
<div class="loader"></div>
|
||||
</template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="menuStore.caches">
|
||||
<component :is="Component"></component>
|
||||
<component v-if="hasAuth" :is="Component"></component>
|
||||
<AnForbidden v-else></AnForbidden>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</a-spin>
|
||||
|
|
@ -75,20 +76,44 @@
|
|||
import { useAppStore } from '@/store/app';
|
||||
import { useMenuStore } from '@/store/menu';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { IconSync } from '@arco-design/web-vue/es/icon';
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import Menu from './Menu.vue';
|
||||
import userDropdown from './UserDropdown.vue';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
defineOptions({ name: 'LayoutPage' });
|
||||
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const menuStore = useMenuStore();
|
||||
const userStore = useUserStore();
|
||||
const isCollapsed = ref(false);
|
||||
const themeConfig = ref({ visible: false });
|
||||
const { toggle, isSupported } = useFullscreen();
|
||||
|
||||
const hasAuth = computed(() => {
|
||||
return route.matched.every(item => {
|
||||
const needAuth = item.meta.auth;
|
||||
const userAuth = userStore.auth;
|
||||
if (needAuth?.includes('*')) {
|
||||
return true;
|
||||
}
|
||||
if (!userStore.accessToken && needAuth?.includes('unlogin')) {
|
||||
return true;
|
||||
}
|
||||
if (!userStore.accessToken) {
|
||||
return false;
|
||||
}
|
||||
if (!needAuth) {
|
||||
return true;
|
||||
}
|
||||
if (userAuth.some(i => needAuth.some(j => j === i))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
icon: 'icon-park-outline-remind',
|
||||
|
|
@ -187,6 +212,30 @@ const buttons = [
|
|||
background-color: #e4ebf1;
|
||||
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||
}
|
||||
|
||||
/* HTML: <div class="loader"></div> */
|
||||
.loader {
|
||||
width: 120px;
|
||||
height: 16px;
|
||||
border-radius: 20px;
|
||||
color: #222;
|
||||
border: 2px solid;
|
||||
position: relative;
|
||||
}
|
||||
.loader::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
margin: 2px;
|
||||
inset: 0 100% 0 0;
|
||||
border-radius: inherit;
|
||||
background: currentColor;
|
||||
animation: l6 2s infinite;
|
||||
}
|
||||
@keyframes l6 {
|
||||
100% {
|
||||
inset: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="json">
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const { component: CategoryTable } = useTable({
|
|||
<div class="flex flex-col overflow-hidden">
|
||||
<span>
|
||||
{record.name}
|
||||
<span class="text-gray-400 text-xs truncate ml-2">@{record.code}</span>
|
||||
<span class="text-orange-500 truncate ml-2">@{record.code}</span>
|
||||
</span>
|
||||
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<template>
|
||||
<bread-page>
|
||||
<a-form :model="{}" :label-col-props="{ span: 3 }" label-align="left" layout="vertical" class="space-y-6">
|
||||
<div>
|
||||
<h2 class="m-0 text-base">常规设置</h2>
|
||||
<p class="text-gray-500 mt-2">首次为你的帐户添加密码时,你需要前往密码重置页面,以便我们验证你的身份。</p>
|
||||
</div>
|
||||
<a-form :model="{}" :label-col-props="{ span: 3 }" label-align="left" layout="vertical" class="space-y-6 mt-4 col-form">
|
||||
<a-form-item label="站点LOGO">
|
||||
<a-avatar :size="64">
|
||||
<img :src="appStore.logo" alt="" />
|
||||
|
|
|
|||
|
|
@ -1,21 +1,17 @@
|
|||
<template>
|
||||
<bread-page>
|
||||
<div class="flex">
|
||||
<a-form
|
||||
:model="{}"
|
||||
:label-col-props="{ span: 3 }"
|
||||
:disabled="!mail.enable"
|
||||
layout="vertical"
|
||||
label-align="left"
|
||||
class="w-[580px]! space-y-6"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<h2 class="m-0 text-base">邮件设置</h2>
|
||||
<p class="text-gray-500 mt-1.5">首次为你的帐户添加密码时,你需要前往密码重置页面,以便我们验证你的身份。</p>
|
||||
</div>
|
||||
<a-form :model="{}" :label-col-props="{ span: 3 }" :disabled="!mail.enable" layout="vertical" label-align="left" class="col-form divide-y space-y-6">
|
||||
<a-form-item label="是否启用" :disabled="false">
|
||||
<a-radio-group v-model="mail.enable">
|
||||
<a-radio :value="true">启用</a-radio>
|
||||
<a-radio :value="false">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
<a-switch v-model="mail.enable"> </a-switch>
|
||||
<template #help> 启用后其他服务可发送邮件通知。 </template>
|
||||
</a-form-item>
|
||||
<a-form-item label="服务器和端口">
|
||||
<a-form-item label="服务器地址和端口" class="pt-6">
|
||||
<a-input v-model="mail.smtpHost" allow-clear placeholder="请输入" class="!w-[314px]"></a-input>
|
||||
<span class="inline-block px-2">:</span>
|
||||
<a-input-number v-model="mail.smtpPort" :min="0" :max="65535" class="w-24!"></a-input-number>
|
||||
|
|
@ -26,42 +22,31 @@
|
|||
<a target="_blank" class="mr-2" href="https://mail.qq.com">QQ邮箱</a>等。
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label="发信人地址">
|
||||
<a-form-item label="发信人地址" class="pt-6">
|
||||
<a-input v-model="mail.smtpFrom" placeholder="请输入" class="!w-[432px]"></a-input>
|
||||
<template #help> 示例: example@mail.com。仅作为发送邮件时的发送人标识,与登陆无关。</template>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否需要验证">
|
||||
<a-radio-group v-model="mail.smtpAuth">
|
||||
<a-radio :value="true">是</a-radio>
|
||||
<a-radio :value="false">否</a-radio>
|
||||
</a-radio-group>
|
||||
<a-form-item label="是否需要验证" class="pt-6">
|
||||
<a-switch v-model="mail.smtpAuth"> </a-switch>
|
||||
<template #help> 可选 </template>
|
||||
</a-form-item>
|
||||
<a-form-item label="验证账号">
|
||||
<a-input
|
||||
:disabled="!mail.enable || !mail.smtpAuth"
|
||||
v-model="mail.smtpUser"
|
||||
placeholder="请输入"
|
||||
class="!w-[432px]"
|
||||
></a-input>
|
||||
<a-form-item label="验证账号" class="pt-6">
|
||||
<a-input :disabled="!mail.enable || !mail.smtpAuth" v-model="mail.smtpUser" placeholder="请输入" class="!w-[432px]"></a-input>
|
||||
<template #help> 示例: example@mail.com。企业邮箱请使用企业域名后缀。</template>
|
||||
</a-form-item>
|
||||
<a-form-item label="验证密码">
|
||||
<a-input
|
||||
:disabled="!mail.enable || !mail.smtpAuth"
|
||||
v-model="mail.smtpPass"
|
||||
placeholder="请输入"
|
||||
class="!w-[432px]"
|
||||
></a-input>
|
||||
<a-form-item label="验证密码" class="pt-6">
|
||||
<a-input :disabled="!mail.enable || !mail.smtpAuth" v-model="mail.smtpPass" placeholder="请输入" class="!w-[432px]"></a-input>
|
||||
<template #help> 示例:AATOLARFABJKYWUY。具体请在对应邮箱设置面板进行生成。 </template>
|
||||
</a-form-item>
|
||||
<a-form-item :disabled="false">
|
||||
<a-form-item :disabled="false" class="pt-2">
|
||||
<a-button type="primary"> 保存修改 </a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<a-divider direction="vertical" :margin="32"></a-divider>
|
||||
<div class="flex-1">
|
||||
<div>
|
||||
<div class="text-base font-semibold">配置测试</div>
|
||||
<div class="text-base font-semibold">邮件测试</div>
|
||||
<div class="text-gray-400 mt-1">发送一封测试邮件,检测邮件设置是否能正常工作。</div>
|
||||
<div class="mt-6">
|
||||
<a-input placeholder="接收人邮箱" class="w-[432px]!"></a-input>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const usernameRender: TableColumnRender = ({ record }) => (
|
|||
<div class="w-full flex-1 overflow-hidden">
|
||||
<div>
|
||||
<span class="cursor-pointer hover:text-brand-500">{record.nickname}</span>
|
||||
<span class="text-gray-400 text-xs truncate ml-2">@{record.username}</span>
|
||||
<span class="text-orange-500 truncate ml-2">@{record.username}</span>
|
||||
</div>
|
||||
<div class="w-full text-gray-400 space-x-4 text-xs">
|
||||
<span>
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ body {
|
|||
margin-top: 8px;
|
||||
}
|
||||
[class^="icon-"] {
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
.arco-menu-item {
|
||||
|
|
@ -159,3 +159,16 @@ body {
|
|||
border-color: var(--color-neutral-2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.col-form {
|
||||
.arco-form-item-wrapper-col {
|
||||
flex-direction: row;
|
||||
}
|
||||
.arco-form-item-content-wrapper {
|
||||
width: 450px;
|
||||
}
|
||||
.arco-form-item-message {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ declare module 'vue' {
|
|||
export interface GlobalComponents {
|
||||
AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete']
|
||||
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
|
||||
ABadge: typeof import('@arco-design/web-vue')['Badge']
|
||||
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
|
||||
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
|
||||
AButton: typeof import('@arco-design/web-vue')['Button']
|
||||
|
|
|
|||
Loading…
Reference in New Issue