chore(release): v1.1.0 unify DI(strategy), AI equivalence service, config domain refactor, docker alias; health checks and schedules
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Content Tab -->
|
||||
<div v-if="editTab === 'content'" class="space-y-4">
|
||||
<div class="border-t-2 border-gray-100 pt-4 first:border-t-0 first:pt-0">
|
||||
<h3 class="mb-3 font-medium">{{ $t('system.diy.imageAdsContent') }}</h3>
|
||||
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- Image List -->
|
||||
<a-form-item :label="$t('system.diy.imageList')">
|
||||
<div class="space-y-2">
|
||||
<draggable
|
||||
v-model="localValue.list"
|
||||
item-key="id"
|
||||
class="space-y-2"
|
||||
@end="handleSort"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div class="flex items-center space-x-2 rounded border p-2">
|
||||
<img
|
||||
:src="element.imageUrl"
|
||||
class="h-16 w-16 rounded object-cover"
|
||||
@click="selectImage(index)"
|
||||
/>
|
||||
<div class="flex-1 space-y-1">
|
||||
<a-input
|
||||
v-model:value="element.title"
|
||||
:placeholder="$t('system.diy.titlePlaceholder')"
|
||||
size="small"
|
||||
/>
|
||||
<a-input
|
||||
v-model:value="element.link.url"
|
||||
:placeholder="$t('system.diy.linkPlaceholder')"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="moveUp(index)"
|
||||
:disabled="index === 0"
|
||||
>
|
||||
<iconify-icon icon="mdi:arrow-up" />
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="moveDown(index)"
|
||||
:disabled="index === localValue.list.length - 1"
|
||||
>
|
||||
<iconify-icon icon="mdi:arrow-down" />
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="removeImage(index)"
|
||||
>
|
||||
<iconify-icon icon="mdi:delete" />
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<a-button
|
||||
type="dashed"
|
||||
class="w-full"
|
||||
@click="addImage"
|
||||
>
|
||||
<iconify-icon icon="mdi:plus" />
|
||||
{{ $t('system.diy.addImage') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Image Size -->
|
||||
<a-form-item :label="$t('system.diy.imageSize')">
|
||||
<a-radio-group v-model:value="localValue.imageSize">
|
||||
<a-radio value="medium">{{ $t('system.diy.medium') }}</a-radio>
|
||||
<a-radio value="large">{{ $t('system.diy.large') }}</a-radio>
|
||||
<a-radio value="custom">{{ $t('system.diy.custom') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Custom Size -->
|
||||
<a-form-item
|
||||
v-if="localValue.imageSize === 'custom'"
|
||||
:label="$t('system.diy.customSize')"
|
||||
>
|
||||
<a-space>
|
||||
<a-input-number
|
||||
v-model:value="localValue.imageWidth"
|
||||
:placeholder="$t('system.diy.width')"
|
||||
:min="50"
|
||||
:max="500"
|
||||
/>
|
||||
<span>×</span>
|
||||
<a-input-number
|
||||
v-model:value="localValue.imageHeight"
|
||||
:placeholder="$t('system.diy.height')"
|
||||
:min="50"
|
||||
:max="500"
|
||||
/>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Image Fill -->
|
||||
<a-form-item :label="$t('system.diy.imageFill')">
|
||||
<a-radio-group v-model:value="localValue.imageFill">
|
||||
<a-radio value="cover">{{ $t('system.diy.cover') }}</a-radio>
|
||||
<a-radio value="contain">{{ $t('system.diy.contain') }}</a-radio>
|
||||
<a-radio value="fill">{{ $t('system.diy.fill') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Show Title -->
|
||||
<a-form-item :label="$t('system.diy.showTitle')">
|
||||
<a-switch v-model:checked="localValue.showTitle" />
|
||||
</a-form-item>
|
||||
|
||||
<!-- Title Position -->
|
||||
<a-form-item
|
||||
v-if="localValue.showTitle"
|
||||
:label="$t('system.diy.titlePosition')"
|
||||
>
|
||||
<a-radio-group v-model:value="localValue.titlePosition">
|
||||
<a-radio value="bottom">{{ $t('system.diy.bottom') }}</a-radio>
|
||||
<a-radio value="overlay">{{ $t('system.diy.overlay') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Style Tab -->
|
||||
<div v-if="editTab === 'style'" class="space-y-4">
|
||||
<ComponentStyleEditor
|
||||
:value="localValue"
|
||||
@update:value="updateLocalValue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import draggable from 'vuedraggable';
|
||||
|
||||
import ComponentStyleEditor from './component-style-editor.vue';
|
||||
|
||||
interface Props {
|
||||
value: any;
|
||||
editTab: 'content' | 'style';
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', value: any): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const localValue = ref<any>({
|
||||
list: [],
|
||||
imageSize: 'medium',
|
||||
imageWidth: 200,
|
||||
imageHeight: 200,
|
||||
imageFill: 'cover',
|
||||
showTitle: true,
|
||||
titlePosition: 'bottom',
|
||||
margin: {
|
||||
top: 0,
|
||||
bottom: 10,
|
||||
both: 0,
|
||||
},
|
||||
topRounded: 0,
|
||||
bottomRounded: 0,
|
||||
componentStartBgColor: '',
|
||||
componentEndBgColor: '',
|
||||
componentGradientAngle: 'to bottom',
|
||||
componentBgUrl: '',
|
||||
componentBgAlpha: 2,
|
||||
isHidden: false,
|
||||
});
|
||||
|
||||
// Image operations
|
||||
const selectImage = (index: number) => {
|
||||
// TODO: Implement image selector
|
||||
console.log('Select image:', index);
|
||||
};
|
||||
|
||||
const addImage = () => {
|
||||
localValue.value.list.push({
|
||||
id: Date.now(),
|
||||
imageUrl: '',
|
||||
title: '',
|
||||
link: {
|
||||
url: '',
|
||||
type: '',
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const removeImage = (index: number) => {
|
||||
localValue.value.list.splice(index, 1);
|
||||
};
|
||||
|
||||
const moveUp = (index: number) => {
|
||||
if (index > 0) {
|
||||
const temp = localValue.value.list[index];
|
||||
localValue.value.list[index] = localValue.value.list[index - 1];
|
||||
localValue.value.list[index - 1] = temp;
|
||||
}
|
||||
};
|
||||
|
||||
const moveDown = (index: number) => {
|
||||
if (index < localValue.value.list.length - 1) {
|
||||
const temp = localValue.value.list[index];
|
||||
localValue.value.list[index] = localValue.value.list[index + 1];
|
||||
localValue.value.list[index + 1] = temp;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSort = () => {
|
||||
// Sorting handled by draggable
|
||||
};
|
||||
|
||||
const updateLocalValue = (newValue: any) => {
|
||||
localValue.value = {
|
||||
...localValue.value,
|
||||
...newValue,
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize local value
|
||||
const initLocalValue = () => {
|
||||
if (props.value) {
|
||||
localValue.value = {
|
||||
...localValue.value,
|
||||
...props.value,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for value changes
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
initLocalValue();
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
// Watch for local changes
|
||||
watch(
|
||||
localValue,
|
||||
(newValue) => {
|
||||
emit('update:value', newValue);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Initialize
|
||||
initLocalValue();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Content Tab -->
|
||||
<div v-if="editTab === 'content'" class="space-y-4">
|
||||
<div class="border-t-2 border-gray-100 pt-4 first:border-t-0 first:pt-0">
|
||||
<h3 class="mb-3 font-medium">{{ $t('system.diy.richTextContent') }}</h3>
|
||||
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- Rich Text Editor -->
|
||||
<a-form-item :label="$t('system.diy.content')">
|
||||
<div class="border rounded">
|
||||
<Toolbar
|
||||
:editor="editorRef"
|
||||
:defaultConfig="toolbarConfig"
|
||||
:mode="'default'"
|
||||
class="border-b"
|
||||
/>
|
||||
<Editor
|
||||
:defaultConfig="editorConfig"
|
||||
:mode="'default'"
|
||||
v-model="localValue.html"
|
||||
@onCreated="handleCreated"
|
||||
class="h-[400px]"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Style Tab -->
|
||||
<div v-if="editTab === 'style'" class="space-y-4">
|
||||
<ComponentStyleEditor
|
||||
:value="localValue"
|
||||
@update:value="updateLocalValue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef, watch, onBeforeUnmount } from 'vue';
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import ComponentStyleEditor from './component-style-editor.vue';
|
||||
|
||||
interface Props {
|
||||
value: any;
|
||||
editTab: 'content' | 'style';
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', value: any): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const editorRef = shallowRef();
|
||||
const localValue = ref<any>({
|
||||
html: '',
|
||||
margin: {
|
||||
top: 0,
|
||||
bottom: 10,
|
||||
both: 0,
|
||||
},
|
||||
topRounded: 0,
|
||||
bottomRounded: 0,
|
||||
componentStartBgColor: '',
|
||||
componentEndBgColor: '',
|
||||
componentGradientAngle: 'to bottom',
|
||||
componentBgUrl: '',
|
||||
componentBgAlpha: 2,
|
||||
isHidden: false,
|
||||
});
|
||||
|
||||
const toolbarConfig = {
|
||||
toolbarKeys: [
|
||||
'headerSelect',
|
||||
'blockquote',
|
||||
'|',
|
||||
'bold',
|
||||
'underline',
|
||||
'italic',
|
||||
'color',
|
||||
'bgColor',
|
||||
'|',
|
||||
'fontSize',
|
||||
'fontFamily',
|
||||
'lineHeight',
|
||||
'|',
|
||||
'bulletedList',
|
||||
'numberedList',
|
||||
'|',
|
||||
'insertLink',
|
||||
'insertImage',
|
||||
'|',
|
||||
'emotion',
|
||||
'|',
|
||||
'undo',
|
||||
'redo',
|
||||
],
|
||||
};
|
||||
|
||||
const editorConfig = {
|
||||
placeholder: '请输入内容...',
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
server: '/api/upload/image',
|
||||
fieldName: 'file',
|
||||
maxFileSize: 2 * 1024 * 1024,
|
||||
allowedFileTypes: ['image/*'],
|
||||
meta: {
|
||||
type: 'rich_text',
|
||||
},
|
||||
customInsert: (res: any, insertFn: Function) => {
|
||||
insertFn(res.data.url);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const handleCreated = (editor: any) => {
|
||||
editorRef.value = editor;
|
||||
};
|
||||
|
||||
const updateLocalValue = (newValue: any) => {
|
||||
localValue.value = {
|
||||
...localValue.value,
|
||||
...newValue,
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize local value
|
||||
const initLocalValue = () => {
|
||||
if (props.value) {
|
||||
localValue.value = {
|
||||
...localValue.value,
|
||||
...props.value,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for value changes
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
initLocalValue();
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
// Watch for local changes
|
||||
watch(
|
||||
localValue,
|
||||
(newValue) => {
|
||||
emit('update:value', newValue);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Destroy editor on unmount
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value;
|
||||
if (editor) {
|
||||
editor.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
initLocalValue();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.w-e-text-container) {
|
||||
min-height: 400px !important;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Content Tab -->
|
||||
<div v-if="editTab === 'content'" class="space-y-4">
|
||||
<div class="border-t-2 border-gray-100 pt-4 first:border-t-0 first:pt-0">
|
||||
<h3 class="mb-3 font-medium">{{ $t('system.diy.content') }}</h3>
|
||||
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- Text Content -->
|
||||
<a-form-item :label="$t('system.diy.textContent')">
|
||||
<a-textarea
|
||||
v-model:value="localValue.text"
|
||||
:placeholder="$t('system.diy.textContentPlaceholder')"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Font Size -->
|
||||
<a-form-item :label="$t('system.diy.fontSize')">
|
||||
<a-slider
|
||||
v-model:value="localValue.fontSize"
|
||||
:min="12"
|
||||
:max="48"
|
||||
:step="1"
|
||||
:marks="{ 12: '12px', 24: '24px', 36: '36px', 48: '48px' }"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Font Weight -->
|
||||
<a-form-item :label="$t('system.diy.fontWeight')">
|
||||
<a-radio-group v-model:value="localValue.fontWeight">
|
||||
<a-radio value="normal">{{ $t('system.diy.normal') }}</a-radio>
|
||||
<a-radio value="bold">{{ $t('system.diy.bold') }}</a-radio>
|
||||
<a-radio value="lighter">{{ $t('system.diy.lighter') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Text Alignment -->
|
||||
<a-form-item :label="$t('system.diy.textAlign')">
|
||||
<a-radio-group v-model:value="localValue.textAlign">
|
||||
<a-radio value="left">{{ $t('system.diy.alignLeft') }}</a-radio>
|
||||
<a-radio value="center">{{ $t('system.diy.alignCenter') }}</a-radio>
|
||||
<a-radio value="right">{{ $t('system.diy.alignRight') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Line Height -->
|
||||
<a-form-item :label="$t('system.diy.lineHeight')">
|
||||
<a-slider
|
||||
v-model:value="localValue.lineHeight"
|
||||
:min="1"
|
||||
:max="3"
|
||||
:step="0.1"
|
||||
:marks="{ 1: '1x', 1.5: '1.5x', 2: '2x', 2.5: '2.5x', 3: '3x' }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Style Tab -->
|
||||
<div v-if="editTab === 'style'" class="space-y-4">
|
||||
<ComponentStyleEditor
|
||||
:value="localValue"
|
||||
@update:value="updateLocalValue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import ComponentStyleEditor from './component-style-editor.vue';
|
||||
|
||||
interface Props {
|
||||
value: any;
|
||||
editTab: 'content' | 'style';
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', value: any): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const localValue = ref<any>({
|
||||
text: '',
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal',
|
||||
textAlign: 'left',
|
||||
lineHeight: 1.5,
|
||||
textColor: '#333333',
|
||||
margin: {
|
||||
top: 0,
|
||||
bottom: 10,
|
||||
both: 0,
|
||||
},
|
||||
topRounded: 0,
|
||||
bottomRounded: 0,
|
||||
componentStartBgColor: '',
|
||||
componentEndBgColor: '',
|
||||
componentGradientAngle: 'to bottom',
|
||||
componentBgUrl: '',
|
||||
componentBgAlpha: 2,
|
||||
isHidden: false,
|
||||
});
|
||||
|
||||
// Initialize local value
|
||||
const initLocalValue = () => {
|
||||
if (props.value) {
|
||||
localValue.value = {
|
||||
...localValue.value,
|
||||
...props.value,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for value changes
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
initLocalValue();
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
// Watch for local changes
|
||||
watch(
|
||||
localValue,
|
||||
(newValue) => {
|
||||
emit('update:value', newValue);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const updateLocalValue = (newValue: any) => {
|
||||
localValue.value = {
|
||||
...localValue.value,
|
||||
...newValue,
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize
|
||||
initLocalValue();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
140
admin-vben/apps/web-antd/src/views/diy/design/data.ts
Normal file
140
admin-vben/apps/web-antd/src/views/diy/design/data.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { VxeGridPropTypes } from 'vxe-table';
|
||||
|
||||
export interface ComponentItem {
|
||||
id: string;
|
||||
componentName: string;
|
||||
componentTitle: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
path: string;
|
||||
uses: number;
|
||||
position?: string;
|
||||
ignore?: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ComponentGroup {
|
||||
title: string;
|
||||
list: Record<string, ComponentItem>;
|
||||
}
|
||||
|
||||
export interface GlobalConfig {
|
||||
title: string;
|
||||
completeLayout: string;
|
||||
completeAlign: string;
|
||||
borderControl: boolean;
|
||||
pageStartBgColor: string;
|
||||
pageEndBgColor: string;
|
||||
pageGradientAngle: string;
|
||||
bgUrl: string;
|
||||
bgHeightScale: number;
|
||||
imgWidth: string;
|
||||
imgHeight: string;
|
||||
topStatusBar: {
|
||||
control: boolean;
|
||||
isShow: boolean;
|
||||
bgColor: string;
|
||||
rollBgColor: string;
|
||||
style: string;
|
||||
styleName: string;
|
||||
textColor: string;
|
||||
rollTextColor: string;
|
||||
textAlign: string;
|
||||
inputPlaceholder: string;
|
||||
imgUrl: string;
|
||||
link: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
bottomTabBar: {
|
||||
control: boolean;
|
||||
isShow: boolean;
|
||||
};
|
||||
popWindow: {
|
||||
imgUrl: string;
|
||||
imgWidth: string;
|
||||
imgHeight: string;
|
||||
count: string;
|
||||
show: number;
|
||||
link: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
template: {
|
||||
textColor: string;
|
||||
pageStartBgColor: string;
|
||||
pageEndBgColor: string;
|
||||
pageGradientAngle: string;
|
||||
componentBgUrl: string;
|
||||
componentBgAlpha: number;
|
||||
componentStartBgColor: string;
|
||||
componentEndBgColor: string;
|
||||
componentGradientAngle: string;
|
||||
topRounded: number;
|
||||
bottomRounded: number;
|
||||
elementBgColor: string;
|
||||
topElementRounded: number;
|
||||
bottomElementRounded: number;
|
||||
margin: {
|
||||
top: number;
|
||||
bottom: number;
|
||||
both: number;
|
||||
};
|
||||
isHidden: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DiyData {
|
||||
id: number;
|
||||
name: string;
|
||||
pageTitle: string;
|
||||
title: string;
|
||||
type: string;
|
||||
typeName: string;
|
||||
templateName: string;
|
||||
isDefault: number;
|
||||
pageMode: string;
|
||||
global: GlobalConfig;
|
||||
value: any[];
|
||||
}
|
||||
|
||||
export interface TemplatePage {
|
||||
title: string;
|
||||
data: {
|
||||
global: GlobalConfig;
|
||||
value: any[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface DesignQuery {
|
||||
id?: string;
|
||||
name?: string;
|
||||
url?: string;
|
||||
type?: string;
|
||||
title?: string;
|
||||
back?: string;
|
||||
}
|
||||
|
||||
export const predefineColors = [
|
||||
'#F4391c',
|
||||
'#ff4500',
|
||||
'#ff8c00',
|
||||
'#FFD009',
|
||||
'#ffd700',
|
||||
'#19C650',
|
||||
'#90ee90',
|
||||
'#00ced1',
|
||||
'#1e90ff',
|
||||
'#c71585',
|
||||
'#FF407E',
|
||||
'#CFAF70',
|
||||
'#A253FF',
|
||||
'rgba(255, 69, 0, 0.68)',
|
||||
'rgb(255, 120, 0)',
|
||||
'hsl(181, 100%, 37%)',
|
||||
'hsla(209, 100%, 56%, 0.73)',
|
||||
'#c7158577'
|
||||
];
|
||||
|
||||
export const positionTypes = ['top_fixed', 'right_fixed', 'bottom_fixed', 'left_fixed', 'fixed'];
|
||||
814
admin-vben/apps/web-antd/src/views/diy/design/index.vue
Normal file
814
admin-vben/apps/web-antd/src/views/diy/design/index.vue
Normal file
@@ -0,0 +1,814 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Header -->
|
||||
<div class="flex h-[60px] items-center bg-primary px-5 text-white">
|
||||
<div class="flex cursor-pointer items-center" @click="goBack">
|
||||
<iconify-icon icon="mdi:arrow-left" class="text-sm" />
|
||||
<span class="ml-2 text-sm">{{ $t('common.back') }}</span>
|
||||
</div>
|
||||
<span class="mx-3 text-white/50">|</span>
|
||||
<span class="text-sm">
|
||||
{{ $t('system.diy.decorating') }}:{{ diyStore.typeName }}
|
||||
</span>
|
||||
|
||||
<div v-if="diyStore.type && diyStore.type !== 'DIY_PAGE'" class="ml-4 flex items-center">
|
||||
<span class="mr-2 text-sm">{{ $t('system.diy.templatePagePlaceholder') }}</span>
|
||||
<a-select
|
||||
v-model:value="template"
|
||||
class="w-[180px]"
|
||||
:placeholder="$t('system.diy.templatePagePlaceholder')"
|
||||
@change="changeTemplatePage"
|
||||
>
|
||||
<a-select-option value="">{{ $t('system.diy.templatePageEmpty') }}</a-select-option>
|
||||
<a-select-option
|
||||
v-for="(item, key) in templatePages"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<div class="flex-1"></div>
|
||||
<a-button @click="preview">{{ $t('common.preview') }}</a-button>
|
||||
<a-button class="ml-2" type="primary" @click="save">{{ $t('common.save') }}</a-button>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex flex-1 bg-gray-50">
|
||||
<!-- Component Library -->
|
||||
<div class="w-[290px] bg-white">
|
||||
<div class="h-full overflow-y-auto p-2">
|
||||
<a-collapse v-model:activeKey="activeNames" class="border-0">
|
||||
<a-collapse-panel
|
||||
v-for="(item, key) in componentGroups"
|
||||
:key="key"
|
||||
:header="item.title"
|
||||
>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div
|
||||
v-for="(compItem, compKey) in item.list"
|
||||
:key="compKey"
|
||||
class="flex cursor-pointer flex-col items-center rounded p-2 text-center hover:bg-blue-50 hover:text-blue-600"
|
||||
:title="compItem.title"
|
||||
@click="addComponent(compKey, compItem)"
|
||||
>
|
||||
<iconify-icon
|
||||
v-if="compItem.icon"
|
||||
:icon="compItem.icon"
|
||||
class="mb-1 text-lg"
|
||||
/>
|
||||
<iconify-icon
|
||||
v-else
|
||||
icon="mdi:palette"
|
||||
class="mb-1 text-lg"
|
||||
/>
|
||||
<span class="text-xs">{{ compItem.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Area -->
|
||||
<div class="relative flex-1 overflow-y-auto p-5">
|
||||
<div class="relative mx-auto w-[375px]">
|
||||
<a-button class="absolute right-0 top-0 z-10" @click="changeCurrentIndex(-99)">
|
||||
{{ $t('system.diy.pageSet') }}
|
||||
</a-button>
|
||||
|
||||
<div class="diy-view-wrap w-[375px] rounded-lg bg-white shadow-lg">
|
||||
<!-- Preview Header -->
|
||||
<div
|
||||
class="preview-head relative h-[64px] cursor-pointer bg-cover bg-center bg-no-repeat"
|
||||
:class="[globalConfig.topStatusBar.style]"
|
||||
:style="{ backgroundColor: globalConfig.topStatusBar.bgColor }"
|
||||
@click="changeCurrentIndex(-99)"
|
||||
>
|
||||
<!-- Style 1: Text Only -->
|
||||
<div
|
||||
v-if="globalConfig.topStatusBar.style === 'style-1' && globalConfig.topStatusBar.isShow"
|
||||
class="content-wrap"
|
||||
>
|
||||
<div
|
||||
class="title-wrap flex h-[30px] items-center justify-center"
|
||||
:style="{
|
||||
fontSize: '14px',
|
||||
color: globalConfig.topStatusBar.textColor,
|
||||
textAlign: globalConfig.topStatusBar.textAlign
|
||||
}"
|
||||
>
|
||||
{{ globalConfig.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Style 2: Image + Text -->
|
||||
<div
|
||||
v-if="globalConfig.topStatusBar.style === 'style-2' && globalConfig.topStatusBar.isShow"
|
||||
class="content-wrap"
|
||||
>
|
||||
<div class="title-wrap flex h-[30px] items-center">
|
||||
<div
|
||||
v-if="globalConfig.topStatusBar.imgUrl"
|
||||
class="mr-2 flex h-[28px] max-w-[150px] items-center"
|
||||
>
|
||||
<img
|
||||
class="max-h-full max-w-full"
|
||||
:src="globalConfig.topStatusBar.imgUrl"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="truncate" :style="{ color: globalConfig.topStatusBar.textColor }">
|
||||
{{ globalConfig.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Style 3: Image + Search -->
|
||||
<div
|
||||
v-if="globalConfig.topStatusBar.style === 'style-3' && globalConfig.topStatusBar.isShow"
|
||||
class="content-wrap flex h-full items-center px-4"
|
||||
>
|
||||
<div
|
||||
v-if="globalConfig.topStatusBar.imgUrl"
|
||||
class="title-wrap mr-3 flex h-[30px] max-w-[85px] items-center"
|
||||
>
|
||||
<img
|
||||
class="max-h-full max-w-full"
|
||||
:src="globalConfig.topStatusBar.imgUrl"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="search relative flex-1 rounded-full bg-white px-8 py-1 text-xs text-gray-500">
|
||||
<iconify-icon icon="mdi:magnify" class="absolute left-2 top-1/2 -translate-y-1/2" />
|
||||
{{ globalConfig.topStatusBar.inputPlaceholder }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Style 4: Location -->
|
||||
<div
|
||||
v-if="globalConfig.topStatusBar.style === 'style-4' && globalConfig.topStatusBar.isShow"
|
||||
class="content-wrap flex h-full items-center px-4"
|
||||
>
|
||||
<iconify-icon icon="mdi:map-marker" class="mr-2" :style="{ color: globalConfig.topStatusBar.textColor }" />
|
||||
<div class="title-wrap mr-2 truncate" :style="{ color: globalConfig.topStatusBar.textColor }">
|
||||
我的位置
|
||||
</div>
|
||||
<iconify-icon icon="mdi:chevron-right" :style="{ color: globalConfig.topStatusBar.textColor }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Content -->
|
||||
<div class="relative min-h-[400px]">
|
||||
<!-- Quick Actions -->
|
||||
<div class="quick-action absolute -right-[70px] top-5 w-[42px] rounded bg-white shadow-md">
|
||||
<a-tooltip placement="right" :title="$t('system.diy.moveUpComponent')">
|
||||
<div class="flex h-[40px] cursor-pointer items-center justify-center hover:bg-gray-50">
|
||||
<iconify-icon icon="mdi:arrow-up" @click="moveUpComponent" />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-tooltip placement="right" :title="$t('system.diy.moveDownComponent')">
|
||||
<div class="flex h-[40px] cursor-pointer items-center justify-center hover:bg-gray-50">
|
||||
<iconify-icon icon="mdi:arrow-down" @click="moveDownComponent" />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-tooltip placement="right" :title="$t('system.diy.copyComponent')">
|
||||
<div class="flex h-[40px] cursor-pointer items-center justify-center hover:bg-gray-50">
|
||||
<iconify-icon icon="mdi:content-copy" @click="copyComponent" />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-tooltip placement="right" :title="$t('system.diy.delComponent')">
|
||||
<div class="flex h-[40px] cursor-pointer items-center justify-center hover:bg-gray-50">
|
||||
<iconify-icon icon="mdi:delete" @click="delComponent" />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-tooltip placement="right" :title="$t('system.diy.resetComponent')">
|
||||
<div class="flex h-[40px] cursor-pointer items-center justify-center hover:bg-gray-50">
|
||||
<iconify-icon icon="mdi:refresh" @click="resetComponent" />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- Iframe Preview -->
|
||||
<iframe
|
||||
v-if="loadingIframe"
|
||||
id="previewIframe"
|
||||
:src="wapPreview"
|
||||
class="preview-iframe h-[600px] w-full"
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
|
||||
<!-- Development Mode -->
|
||||
<div v-if="loadingDev" class="p-5">
|
||||
<div class="mb-5 text-xl font-bold">{{ $t('system.diy.developTitle') }}</div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-2 text-sm">{{ $t('system.diy.wapDomain') }}</div>
|
||||
<a-input
|
||||
v-model:value="wapDomain"
|
||||
:placeholder="$t('system.diy.wapDomainPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="saveWapDomain">
|
||||
{{ $t('common.confirm') }}
|
||||
</a-button>
|
||||
<a-button @click="settingTips">
|
||||
{{ $t('system.diy.settingTips') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Property Editor -->
|
||||
<div class="w-[400px] bg-white">
|
||||
<div class="h-full overflow-y-auto">
|
||||
<a-card class="border-0 shadow-none">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex-1">
|
||||
{{ currentIndex === -99 ? $t('system.diy.pageSet') : editComponent?.componentTitle }}
|
||||
</span>
|
||||
<div v-if="currentComponent" class="flex rounded-full bg-gray-100 text-sm">
|
||||
<span
|
||||
class="cursor-pointer rounded-full px-4 py-1"
|
||||
:class="{ 'bg-primary text-white': editTab === 'content' }"
|
||||
@click="editTab = 'content'"
|
||||
>
|
||||
{{ $t('system.diy.tabEditContent') }}
|
||||
</span>
|
||||
<span
|
||||
class="cursor-pointer rounded-full px-4 py-1"
|
||||
:class="{ 'bg-primary text-white': editTab === 'style' }"
|
||||
@click="editTab = 'style'"
|
||||
>
|
||||
{{ $t('system.diy.tabEditStyle') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="edit-component-wrap">
|
||||
<!-- Page Settings -->
|
||||
<div v-if="currentIndex === -99">
|
||||
<PageSettingsForm
|
||||
:global-config="globalConfig"
|
||||
@update:global-config="updateGlobalConfig"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Component Editor -->
|
||||
<div v-else-if="currentComponent">
|
||||
<component
|
||||
:is="currentComponent"
|
||||
:value="componentValue"
|
||||
:edit-tab="editTab"
|
||||
@update:value="updateComponentValue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { useI18n } from '#/hooks';
|
||||
import { getDiyTemplatePages, addDiyPage, editDiyPage, initDiyPage } from '#/api';
|
||||
|
||||
import type { ComponentGroup, GlobalConfig, TemplatePage, DesignQuery } from './data';
|
||||
import { predefineColors, positionTypes } from './data';
|
||||
|
||||
import PageSettingsForm from './modules/page-settings-form.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// Route query parameters
|
||||
const query = computed<DesignQuery>(() => ({
|
||||
id: route.query.id as string,
|
||||
name: route.query.name as string,
|
||||
url: route.query.url as string,
|
||||
type: route.query.type as string,
|
||||
title: route.query.title as string,
|
||||
back: (route.query.back as string) || '/diy/list',
|
||||
}));
|
||||
|
||||
// Component data
|
||||
const template = ref('');
|
||||
const oldTemplate = ref('');
|
||||
const wapDomain = ref('');
|
||||
const wapUrl = ref('');
|
||||
const wapPreview = ref('');
|
||||
const loadingIframe = ref(false);
|
||||
const loadingDev = ref(false);
|
||||
const timeIframe = ref(0);
|
||||
const difference = ref(0);
|
||||
const uniAppLoadStatus = ref(false);
|
||||
const isRepeat = ref(false);
|
||||
|
||||
const componentGroups = ref<Record<string, ComponentGroup>>({});
|
||||
const templatePages = ref<Record<string, TemplatePage>>({});
|
||||
const activeNames = ref<string[]>([]);
|
||||
const editTab = ref<'content' | 'style'>('content');
|
||||
|
||||
// DIY data
|
||||
const diyData = reactive({
|
||||
id: 0,
|
||||
name: '',
|
||||
pageTitle: '',
|
||||
title: '',
|
||||
type: '',
|
||||
typeName: '',
|
||||
templateName: '',
|
||||
isDefault: 0,
|
||||
pageMode: 'diy',
|
||||
global: {} as GlobalConfig,
|
||||
value: [] as any[],
|
||||
});
|
||||
|
||||
const globalConfig = computed<GlobalConfig>(() => diyData.global);
|
||||
const currentIndex = ref(-99);
|
||||
const currentComponent = ref('');
|
||||
const componentValue = computed(() =>
|
||||
currentIndex.value >= 0 ? diyData.value[currentIndex.value] : null
|
||||
);
|
||||
const editComponent = computed(() =>
|
||||
currentIndex.value === -99 ? globalConfig.value : diyData.value[currentIndex.value]
|
||||
);
|
||||
|
||||
// Original data for change detection
|
||||
const originData = reactive({
|
||||
id: 0,
|
||||
name: '',
|
||||
pageTitle: '',
|
||||
title: '',
|
||||
value: '',
|
||||
});
|
||||
|
||||
const isChange = computed(() => {
|
||||
const currentData = {
|
||||
id: diyData.id,
|
||||
name: diyData.name,
|
||||
pageTitle: diyData.pageTitle,
|
||||
title: diyData.global.title,
|
||||
value: JSON.stringify({
|
||||
global: diyData.global,
|
||||
value: diyData.value,
|
||||
}),
|
||||
};
|
||||
return JSON.stringify(currentData) === JSON.stringify(originData);
|
||||
});
|
||||
|
||||
// Load template pages
|
||||
const loadTemplatePages = async (type: string) => {
|
||||
try {
|
||||
const res = await getDiyTemplatePages({
|
||||
type,
|
||||
mode: 'diy',
|
||||
});
|
||||
templatePages.value = res.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to load template pages:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize page data
|
||||
const initPageData = async () => {
|
||||
try {
|
||||
const res = await initDiyPage({
|
||||
id: query.value.id,
|
||||
name: query.value.name,
|
||||
url: query.value.url,
|
||||
type: query.value.type,
|
||||
title: query.value.title,
|
||||
});
|
||||
|
||||
const data = res.data;
|
||||
|
||||
// Initialize DIY data
|
||||
diyData.id = data.id || 0;
|
||||
diyData.name = data.name;
|
||||
diyData.pageTitle = data.page_title;
|
||||
diyData.type = data.type;
|
||||
diyData.typeName = data.type_name;
|
||||
diyData.templateName = data.template;
|
||||
diyData.isDefault = data.is_default;
|
||||
diyData.pageMode = data.mode;
|
||||
|
||||
template.value = data.template;
|
||||
|
||||
// Initialize global config
|
||||
if (data.global) {
|
||||
Object.assign(diyData.global, data.global);
|
||||
}
|
||||
|
||||
// Initialize value
|
||||
if (data.value) {
|
||||
const sources = JSON.parse(data.value);
|
||||
diyData.global = sources.global;
|
||||
if (sources.value.length) {
|
||||
diyData.value = sources.value;
|
||||
}
|
||||
} else {
|
||||
diyData.global.title = data.title;
|
||||
}
|
||||
|
||||
// Set original data
|
||||
originData.id = diyData.id;
|
||||
originData.name = diyData.name;
|
||||
originData.pageTitle = diyData.pageTitle;
|
||||
originData.title = diyData.global.title;
|
||||
originData.value = JSON.stringify({
|
||||
global: diyData.global,
|
||||
value: diyData.value,
|
||||
});
|
||||
|
||||
// Load components
|
||||
componentGroups.value = data.component;
|
||||
activeNames.value = Object.keys(data.component);
|
||||
|
||||
// Load template pages
|
||||
await loadTemplatePages(data.type);
|
||||
|
||||
// Setup preview
|
||||
wapDomain.value = data.domain_url.wap_domain;
|
||||
wapUrl.value = data.domain_url.wap_url;
|
||||
|
||||
setupPreview();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize page:', error);
|
||||
message.error(t('system.diy.initPageError'));
|
||||
}
|
||||
};
|
||||
|
||||
// Setup preview
|
||||
const setupPreview = () => {
|
||||
wapPreview.value = `${wapUrl.value}${query.value.url}?mode=decorate`;
|
||||
|
||||
const sendMessage = () => {
|
||||
timeIframe.value = Date.now();
|
||||
postMessageToIframe();
|
||||
};
|
||||
|
||||
// Send initial message
|
||||
sendMessage();
|
||||
|
||||
// Send messages periodically if no response
|
||||
let sendCount = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (uniAppLoadStatus.value || sendCount >= 50) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
sendMessage();
|
||||
sendCount++;
|
||||
}, 200);
|
||||
|
||||
// Show development mode if iframe doesn't load in 10 seconds
|
||||
setTimeout(() => {
|
||||
if (difference.value === 0) {
|
||||
loadingDev.value = true;
|
||||
loadingIframe.value = false;
|
||||
}
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
// Post message to iframe
|
||||
const postMessageToIframe = () => {
|
||||
const data = JSON.stringify({
|
||||
type: 'appOnReady',
|
||||
message: '加载完成',
|
||||
global: diyData.global,
|
||||
value: diyData.value,
|
||||
currentIndex: currentIndex.value,
|
||||
});
|
||||
|
||||
const iframe = document.getElementById('previewIframe') as HTMLIFrameElement;
|
||||
if (iframe && iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage(data, '*');
|
||||
}
|
||||
};
|
||||
|
||||
// Message event handler
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
try {
|
||||
let data = { type: '' };
|
||||
if (typeof event.data === 'string') {
|
||||
data = JSON.parse(event.data);
|
||||
} else if (typeof event.data === 'object') {
|
||||
data = event.data;
|
||||
}
|
||||
|
||||
if (!data.type) return;
|
||||
|
||||
switch (data.type) {
|
||||
case 'appOnLaunch':
|
||||
case 'appOnReady':
|
||||
loadingDev.value = false;
|
||||
loadingIframe.value = true;
|
||||
const loadTime = Date.now();
|
||||
difference.value = loadTime - timeIframe.value;
|
||||
uniAppLoadStatus.value = true;
|
||||
break;
|
||||
case 'init':
|
||||
postMessageToIframe();
|
||||
break;
|
||||
case 'change':
|
||||
changeCurrentIndex(data.index, data.component);
|
||||
break;
|
||||
case 'data':
|
||||
changeCurrentIndex(data.index, data.component);
|
||||
diyData.global = data.global;
|
||||
diyData.value = data.value;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Message handling error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Component operations
|
||||
const addComponent = (key: string, component: any) => {
|
||||
// Implementation for adding component
|
||||
console.log('Add component:', key, component);
|
||||
};
|
||||
|
||||
const changeCurrentIndex = (index: number, component?: any) => {
|
||||
currentIndex.value = index;
|
||||
if (index === -99) {
|
||||
currentComponent.value = 'page-settings';
|
||||
} else if (component) {
|
||||
currentComponent.value = component.path;
|
||||
}
|
||||
};
|
||||
|
||||
const moveUpComponent = () => {
|
||||
if (currentIndex.value <= 0) return;
|
||||
// Implementation for moving component up
|
||||
};
|
||||
|
||||
const moveDownComponent = () => {
|
||||
if (currentIndex.value >= diyData.value.length - 1) return;
|
||||
// Implementation for moving component down
|
||||
};
|
||||
|
||||
const copyComponent = () => {
|
||||
if (currentIndex.value < 0) return;
|
||||
// Implementation for copying component
|
||||
};
|
||||
|
||||
const delComponent = () => {
|
||||
if (currentIndex.value < 0) return;
|
||||
|
||||
Modal.confirm({
|
||||
title: t('common.warning'),
|
||||
content: t('system.diy.delComponentTips'),
|
||||
onOk: () => {
|
||||
diyData.value.splice(currentIndex.value, 1);
|
||||
if (diyData.value.length === 0) {
|
||||
currentIndex.value = -99;
|
||||
} else if (currentIndex.value >= diyData.value.length) {
|
||||
currentIndex.value = diyData.value.length - 1;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const resetComponent = () => {
|
||||
if (currentIndex.value < 0) return;
|
||||
// Implementation for resetting component
|
||||
};
|
||||
|
||||
const updateGlobalConfig = (config: GlobalConfig) => {
|
||||
diyData.global = config;
|
||||
};
|
||||
|
||||
const updateComponentValue = (value: any) => {
|
||||
if (currentIndex.value >= 0) {
|
||||
diyData.value[currentIndex.value] = value;
|
||||
}
|
||||
};
|
||||
|
||||
// Template page change
|
||||
const changeTemplatePage = (value: string) => {
|
||||
if (diyData.value.length > 0) {
|
||||
Modal.confirm({
|
||||
title: t('common.warning'),
|
||||
content: t('system.diy.changeTemplatePageTips'),
|
||||
onOk: () => {
|
||||
changeCurrentIndex(-99);
|
||||
if (value && templatePages.value[value]) {
|
||||
const data = cloneDeep(templatePages.value[value].data);
|
||||
diyData.global = data.global;
|
||||
diyData.value = data.value;
|
||||
} else {
|
||||
diyData.value = [];
|
||||
diyData.global.title = query.value.title || '';
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
template.value = oldTemplate.value;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
changeCurrentIndex(-99);
|
||||
if (value && templatePages.value[value]) {
|
||||
const data = cloneDeep(templatePages.value[value].data);
|
||||
diyData.global = data.global;
|
||||
diyData.value = data.value;
|
||||
} else {
|
||||
diyData.value = [];
|
||||
diyData.global.title = query.value.title || '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Save WAP domain
|
||||
const saveWapDomain = () => {
|
||||
if (!wapDomain.value.trim()) {
|
||||
message.warning(t('system.diy.wapDomainPlaceholder'));
|
||||
return;
|
||||
}
|
||||
|
||||
wapUrl.value = wapDomain.value + '/wap';
|
||||
localStorage.setItem('wap_domain', wapUrl.value);
|
||||
loadingIframe.value = true;
|
||||
loadingDev.value = false;
|
||||
setupPreview();
|
||||
};
|
||||
|
||||
// Preview
|
||||
const preview = () => {
|
||||
save((id: number) => {
|
||||
const pageId = diyData.id || id;
|
||||
const url = router.resolve({
|
||||
path: '/site/preview/wap',
|
||||
query: { page: `${query.value.url}?id=${pageId}` },
|
||||
});
|
||||
window.open(url.href, '_blank');
|
||||
});
|
||||
};
|
||||
|
||||
// Save
|
||||
const save = async (callback?: (id: number) => void) => {
|
||||
// Validate
|
||||
if (!diyData.pageTitle) {
|
||||
message.warning(t('system.diy.diyPageTitlePlaceholder'));
|
||||
changeCurrentIndex(-99);
|
||||
return;
|
||||
}
|
||||
|
||||
if (diyData.global.popWindow.show && !diyData.global.popWindow.imgUrl) {
|
||||
message.warning('请上传弹窗图片');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRepeat.value) return;
|
||||
isRepeat.value = true;
|
||||
|
||||
try {
|
||||
diyData.templateName = template.value;
|
||||
|
||||
const data = {
|
||||
id: diyData.id,
|
||||
name: diyData.name,
|
||||
page_title: diyData.pageTitle,
|
||||
title: diyData.global.title,
|
||||
type: diyData.type,
|
||||
template: diyData.templateName,
|
||||
is_default: diyData.isDefault,
|
||||
is_change: isChange.value ? 0 : 1,
|
||||
value: JSON.stringify({
|
||||
global: diyData.global,
|
||||
value: diyData.value,
|
||||
}),
|
||||
};
|
||||
|
||||
const api = diyData.id ? editDiyPage : addDiyPage;
|
||||
const res = await api(data);
|
||||
|
||||
if (res.code === 1) {
|
||||
if (diyData.id) {
|
||||
// Update existing page
|
||||
message.success(t('common.saveSuccess'));
|
||||
} else {
|
||||
// Create new page
|
||||
router.push(query.value.back);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(res.data.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
message.error(t('common.saveError'));
|
||||
} finally {
|
||||
isRepeat.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Go back
|
||||
const goBack = () => {
|
||||
if (isChange.value) {
|
||||
router.push(query.value.back);
|
||||
} else {
|
||||
Modal.confirm({
|
||||
title: t('common.warning'),
|
||||
content: t('system.diy.leavePageContentTips'),
|
||||
onOk: () => {
|
||||
router.push(query.value.back);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const settingTips = () => {
|
||||
window.open('https://www.kancloud.cn/niucloud/niucloud-admin-develop/3213393', '_blank');
|
||||
};
|
||||
|
||||
// Watch template changes
|
||||
watch(template, (newValue, oldValue) => {
|
||||
oldTemplate.value = oldValue;
|
||||
});
|
||||
|
||||
// Watch data changes
|
||||
watch(
|
||||
() => diyData,
|
||||
() => {
|
||||
postMessageToIframe();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', handleMessage);
|
||||
initPageData();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-collapse-borderless) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:deep(.ant-collapse-header) {
|
||||
padding: 12px 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-collapse-content) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.preview-head {
|
||||
background-image: url('/src/assets/images/diy_preview_head.png');
|
||||
}
|
||||
|
||||
.preview-head.style-1 .content-wrap {
|
||||
@apply flex h-full items-center justify-center;
|
||||
}
|
||||
|
||||
.preview-head.style-2 .content-wrap {
|
||||
@apply flex h-full items-center px-4;
|
||||
}
|
||||
|
||||
.preview-head.style-3 .content-wrap {
|
||||
@apply flex h-full items-center px-4;
|
||||
}
|
||||
|
||||
.preview-head.style-4 .content-wrap {
|
||||
@apply flex h-full items-center px-4;
|
||||
}
|
||||
|
||||
.quick-action > div {
|
||||
@apply border-b border-gray-100 last:border-b-0;
|
||||
}
|
||||
|
||||
.diy-view-wrap {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
|
||||
.edit-component-wrap {
|
||||
@apply p-4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Background Settings -->
|
||||
<div class="border-t-2 border-gray-100 pt-4 first:border-t-0 first:pt-0">
|
||||
<h3 class="mb-3 font-medium">{{ $t('system.diy.background') }}</h3>
|
||||
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- Background Color -->
|
||||
<a-form-item v-if="!ignore.includes('componentBgColor')" :label="$t('system.diy.bgColor')">
|
||||
<div class="flex items-center space-x-2">
|
||||
<a-color-picker v-model:value="localValue.componentStartBgColor" show-alpha />
|
||||
<iconify-icon icon="mdi:arrow-right" class="text-gray-400" />
|
||||
<a-color-picker v-model:value="localValue.componentEndBgColor" show-alpha />
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-500">{{ $t('system.diy.bgColorTips') }}</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Gradient Angle -->
|
||||
<a-form-item v-if="!ignore.includes('componentBgColor')" :label="$t('system.diy.gradientAngle')">
|
||||
<a-radio-group v-model:value="localValue.componentGradientAngle">
|
||||
<a-radio value="to bottom">{{ $t('system.diy.topToBottom') }}</a-radio>
|
||||
<a-radio value="to right">{{ $t('system.diy.leftToRight') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Background Image -->
|
||||
<a-form-item v-if="!ignore.includes('componentBgUrl')" :label="$t('system.diy.bgImage')">
|
||||
<ImageUpload
|
||||
v-model:value="localValue.componentBgUrl"
|
||||
:max-count="1"
|
||||
:show-upload-list="false"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Background Alpha -->
|
||||
<a-form-item v-if="!ignore.includes('componentBgUrl') && localValue.componentBgUrl" :label="$t('system.diy.bgAlpha')">
|
||||
<a-slider
|
||||
v-model:value="localValue.componentBgAlpha"
|
||||
:min="0"
|
||||
:max="10"
|
||||
:step="1"
|
||||
:marks="{ 0: '0%', 5: '50%', 10: '100%' }"
|
||||
/>
|
||||
<div class="text-xs text-gray-500">{{ $t('system.diy.bgAlphaTips') }}</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- Spacing Settings -->
|
||||
<div class="border-t-2 border-gray-100 pt-4">
|
||||
<h3 class="mb-3 font-medium">{{ $t('system.diy.spacing') }}</h3>
|
||||
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- Top Margin -->
|
||||
<a-form-item v-if="!ignore.includes('marginTop')" :label="$t('system.diy.marginTop')">
|
||||
<a-slider
|
||||
v-model:value="localValue.margin.top"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:marks="{ '-100': '-100', 0: '0', 100: '100' }"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Bottom Margin -->
|
||||
<a-form-item v-if="!ignore.includes('marginBottom')" :label="$t('system.diy.marginBottom')">
|
||||
<a-slider
|
||||
v-model:value="localValue.margin.bottom"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:marks="{ 0: '0', 50: '50', 100: '100' }"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Side Margin -->
|
||||
<a-form-item v-if="!ignore.includes('marginBoth')" :label="$t('system.diy.marginBoth')">
|
||||
<a-slider
|
||||
v-model:value="localValue.margin.both"
|
||||
:min="0"
|
||||
:max="50"
|
||||
:step="1"
|
||||
:marks="{ 0: '0', 25: '25', 50: '50' }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- Border Settings -->
|
||||
<div class="border-t-2 border-gray-100 pt-4">
|
||||
<h3 class="mb-3 font-medium">{{ $t('system.diy.border') }}</h3>
|
||||
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- Top Rounded -->
|
||||
<a-form-item v-if="!ignore.includes('topRounded')" :label="$t('system.diy.topRounded')">
|
||||
<a-slider
|
||||
v-model:value="localValue.topRounded"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:marks="{ 0: '0', 50: '50%', 100: '100%' }"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Bottom Rounded -->
|
||||
<a-form-item v-if="!ignore.includes('bottomRounded')" :label="$t('system.diy.bottomRounded')">
|
||||
<a-slider
|
||||
v-model:value="localValue.bottomRounded"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:marks="{ 0: '0', 50: '50%', 100: '100%' }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- Element Settings -->
|
||||
<div class="border-t-2 border-gray-100 pt-4">
|
||||
<h3 class="mb-3 font-medium">{{ $t('system.diy.element') }}</h3>
|
||||
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- Element Background Color -->
|
||||
<a-form-item v-if="!ignore.includes('elementBgColor')" :label="$t('system.diy.elementBgColor')">
|
||||
<a-color-picker v-model:value="localValue.elementBgColor" show-alpha />
|
||||
</a-form-item>
|
||||
|
||||
<!-- Element Top Rounded -->
|
||||
<a-form-item v-if="!ignore.includes('topElementRounded')" :label="$t('system.diy.elementTopRounded')">
|
||||
<a-slider
|
||||
v-model:value="localValue.topElementRounded"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:marks="{ 0: '0', 50: '50%', 100: '100%' }"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Element Bottom Rounded -->
|
||||
<a-form-item v-if="!ignore.includes('bottomElementRounded')" :label="$t('system.diy.elementBottomRounded')">
|
||||
<a-slider
|
||||
v-model:value="localValue.bottomElementRounded"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:marks="{ 0: '0', 50: '50%', 100: '100%' }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- Visibility Settings -->
|
||||
<div class="border-t-2 border-gray-100 pt-4">
|
||||
<h3 class="mb-3 font-medium">{{ $t('system.diy.visibility') }}</h3>
|
||||
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- Hide Component -->
|
||||
<a-form-item v-if="!ignore.includes('isHidden')" :label="$t('system.diy.hideComponent')">
|
||||
<a-switch v-model:checked="localValue.isHidden" />
|
||||
<div class="mt-1 text-xs text-gray-500">{{ $t('system.diy.hideComponentTips') }}</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
interface Props {
|
||||
value: any;
|
||||
ignore?: string[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', value: any): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
ignore: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const localValue = ref<any>({
|
||||
componentStartBgColor: '',
|
||||
componentEndBgColor: '',
|
||||
componentGradientAngle: 'to bottom',
|
||||
componentBgUrl: '',
|
||||
componentBgAlpha: 2,
|
||||
margin: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
both: 0,
|
||||
},
|
||||
topRounded: 0,
|
||||
bottomRounded: 0,
|
||||
elementBgColor: '',
|
||||
topElementRounded: 0,
|
||||
bottomElementRounded: 0,
|
||||
isHidden: false,
|
||||
});
|
||||
|
||||
// Initialize local value
|
||||
const initLocalValue = () => {
|
||||
if (props.value) {
|
||||
localValue.value = {
|
||||
...localValue.value,
|
||||
...props.value,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for value changes
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
initLocalValue();
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
// Watch for local changes
|
||||
watch(
|
||||
localValue,
|
||||
(newValue) => {
|
||||
emit('update:value', newValue);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Initialize
|
||||
initLocalValue();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Page Settings -->
|
||||
<div class="border-t-2 border-gray-100 pt-4 first:border-t-0 first:pt-0">
|
||||
<h3 class="mb-3 font-medium">{{ $t('system.diy.pageSettings') }}</h3>
|
||||
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- Page Title -->
|
||||
<a-form-item :label="$t('system.diy.pageTitle')">
|
||||
<a-input
|
||||
v-model:value="localConfig.title"
|
||||
:placeholder="$t('system.diy.pageTitlePlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Page Name -->
|
||||
<a-form-item :label="$t('system.diy.pageName')">
|
||||
<a-input
|
||||
v-model:value="localPageName"
|
||||
:placeholder="$t('system.diy.pageNamePlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Page Mode -->
|
||||
<a-form-item :label="$t('system.diy.pageMode')">
|
||||
<a-radio-group v-model:value="localConfig.completeLayout">
|
||||
<a-radio value="style-1">{{ $t('system.diy.style1') }}</a-radio>
|
||||
<a-radio value="style-2">{{ $t('system.diy.style2') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Alignment -->
|
||||
<a-form-item v-if="localConfig.completeLayout === 'style-2'" :label="$t('system.diy.alignment')">
|
||||
<a-radio-group v-model:value="localConfig.completeAlign">
|
||||
<a-radio value="left">{{ $t('system.diy.alignLeft') }}</a-radio>
|
||||
<a-radio value="right">{{ $t('system.diy.alignRight') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Border Control -->
|
||||
<a-form-item :label="$t('system.diy.borderControl')">
|
||||
<a-switch v-model:checked="localConfig.borderControl" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- Page Background -->
|
||||
<div class="border-t-2 border-gray-100 pt-4">
|
||||
<h3 class="mb-3 font-medium">{{ $t('system.diy.pageBackground') }}</h3>
|
||||
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- Background Color -->
|
||||
<a-form-item :label="$t('system.diy.bgColor')">
|
||||
<div class="flex items-center space-x-2">
|
||||
<a-color-picker v-model:value="localConfig.pageStartBgColor" show-alpha />
|
||||
<iconify-icon icon="mdi:arrow-right" class="text-gray-400" />
|
||||
<a-color-picker v-model:value="localConfig.pageEndBgColor" show-alpha />
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-500">{{ $t('system.diy.bgColorTips') }}</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Gradient Angle -->
|
||||
<a-form-item :label="$t('system.diy.gradientAngle')">
|
||||
<a-radio-group v-model:value="localConfig.pageGradientAngle">
|
||||
<a-radio value="to bottom">{{ $t('system.diy.topToBottom') }}</a-radio>
|
||||
<a-radio value="to right">{{ $t('system.diy.leftToRight') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Background Image -->
|
||||
<a-form-item :label="$t('system.diy.bgImage')">
|
||||
<ImageUpload
|
||||
v-model:value="localConfig.bgUrl"
|
||||
:max-count="1"
|
||||
:show-upload-list="false"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Background Height Scale -->
|
||||
<a-form-item :label="$t('system.diy.bgHeightScale')">
|
||||
<a-slider
|
||||
v-model:value="localConfig.bgHeightScale"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="5"
|
||||
:marks="{ 0: '0%', 50: '50%', 100: '100%' }"
|
||||
/>
|
||||
<div class="text-xs text-gray-500">{{ $t('system.diy.bgHeightScaleTips') }}</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- Top Status Bar -->
|
||||
<div class="border-t-2 border-gray-100 pt-4">
|
||||
<h3 class="mb-3 font-medium">{{ $t('system.diy.topStatusBar') }}</h3>
|
||||
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- Show Status Bar -->
|
||||
<a-form-item :label="$t('system.diy.showStatusBar')">
|
||||
<a-switch v-model:checked="localConfig.topStatusBar.isShow" />
|
||||
</a-form-item>
|
||||
|
||||
<!-- Background Color -->
|
||||
<a-form-item :label="$t('system.diy.bgColor')">
|
||||
<a-color-picker v-model:value="localConfig.topStatusBar.bgColor" show-alpha />
|
||||
</a-form-item>
|
||||
|
||||
<!-- Text Color -->
|
||||
<a-form-item :label="$t('system.diy.textColor')">
|
||||
<a-color-picker v-model:value="localConfig.topStatusBar.textColor" show-alpha />
|
||||
</a-form-item>
|
||||
|
||||
<!-- Status Bar Style -->
|
||||
<a-form-item :label="$t('system.diy.statusBarStyle')">
|
||||
<a-radio-group v-model:value="localConfig.topStatusBar.style">
|
||||
<a-radio value="style-1">{{ $t('system.diy.style1Text') }}</a-radio>
|
||||
<a-radio value="style-2">{{ $t('system.diy.style2ImageText') }}</a-radio>
|
||||
<a-radio value="style-3">{{ $t('system.diy.style3ImageSearch') }}</a-radio>
|
||||
<a-radio value="style-4">{{ $t('system.diy.style4Location') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Text Alignment -->
|
||||
<a-form-item v-if="localConfig.topStatusBar.style === 'style-1'" :label="$t('system.diy.textAlign')">
|
||||
<a-radio-group v-model:value="localConfig.topStatusBar.textAlign">
|
||||
<a-radio value="left">{{ $t('system.diy.alignLeft') }}</a-radio>
|
||||
<a-radio value="center">{{ $t('system.diy.alignCenter') }}</a-radio>
|
||||
<a-radio value="right">{{ $t('system.diy.alignRight') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Logo Image -->
|
||||
<a-form-item v-if="['style-2', 'style-3'].includes(localConfig.topStatusBar.style)" :label="$t('system.diy.logoImage')">
|
||||
<ImageUpload
|
||||
v-model:value="localConfig.topStatusBar.imgUrl"
|
||||
:max-count="1"
|
||||
:show-upload-list="false"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Search Placeholder -->
|
||||
<a-form-item v-if="localConfig.topStatusBar.style === 'style-3'" :label="$t('system.diy.searchPlaceholder')">
|
||||
<a-input
|
||||
v-model:value="localConfig.topStatusBar.inputPlaceholder"
|
||||
:placeholder="$t('system.diy.searchPlaceholderTips')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Link -->
|
||||
<a-form-item :label="$t('system.diy.link')">
|
||||
<LinkSelector
|
||||
v-model:value="localConfig.topStatusBar.link"
|
||||
:placeholder="$t('system.diy.linkPlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- Pop Window -->
|
||||
<div class="border-t-2 border-gray-100 pt-4">
|
||||
<h3 class="mb-3 font-medium">{{ $t('system.diy.popWindow') }}</h3>
|
||||
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- Show Pop Window -->
|
||||
<a-form-item :label="$t('system.diy.showPopWindow')">
|
||||
<a-radio-group v-model:value="localConfig.popWindow.count">
|
||||
<a-radio value="-1">{{ $t('system.diy.neverShow') }}</a-radio>
|
||||
<a-radio value="once">{{ $t('system.diy.showOnce') }}</a-radio>
|
||||
<a-radio value="always">{{ $t('system.diy.showAlways') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Pop Window Image -->
|
||||
<a-form-item v-if="localConfig.popWindow.count !== '-1'" :label="$t('system.diy.popImage')">
|
||||
<ImageUpload
|
||||
v-model:value="localConfig.popWindow.imgUrl"
|
||||
:max-count="1"
|
||||
:show-upload-list="false"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Pop Window Link -->
|
||||
<a-form-item v-if="localConfig.popWindow.count !== '-1'" :label="$t('system.diy.popLink')">
|
||||
<LinkSelector
|
||||
v-model:value="localConfig.popWindow.link"
|
||||
:placeholder="$t('system.diy.popLinkPlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { GlobalConfig } from '../data';
|
||||
|
||||
interface Props {
|
||||
globalConfig: GlobalConfig;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:globalConfig', config: GlobalConfig): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const localConfig = ref<GlobalConfig>(cloneDeep(props.globalConfig));
|
||||
const localPageName = ref('');
|
||||
|
||||
// Watch for changes and emit
|
||||
watch(
|
||||
localConfig,
|
||||
(newConfig) => {
|
||||
emit('update:globalConfig', newConfig);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Watch for props changes
|
||||
watch(
|
||||
() => props.globalConfig,
|
||||
(newConfig) => {
|
||||
localConfig.value = cloneDeep(newConfig);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user