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:
wanwu
2025-11-14 02:34:06 +08:00
parent e54041331a
commit de821ae5fd
1501 changed files with 60179 additions and 21496 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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'];

View 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>

View File

@@ -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>

View File

@@ -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>