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,181 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SiteApi } from '#/api';
import { Avatar, Space, Tooltip } from 'ant-design-vue';
import { Icon } from '@vben/icons';
import { $t } from '#/locales';
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'group_name',
label: $t('site.group.groupName'),
rules: 'required|max:20',
componentProps: {
placeholder: $t('site.group.groupNamePlaceholder'),
},
},
{
component: 'Textarea',
fieldName: 'group_desc',
label: $t('site.group.groupDesc'),
rules: 'max:100',
componentProps: {
placeholder: $t('site.group.groupDescPlaceholder'),
rows: 3,
},
},
{
component: 'CheckboxGroup',
fieldName: 'app',
label: $t('site.group.mainApp'),
rules: 'required',
componentProps: {
placeholder: $t('site.group.mainAppPlaceholder'),
},
},
{
component: 'CheckboxGroup',
fieldName: 'addon',
label: $t('site.group.containAddon'),
componentProps: {
placeholder: $t('site.group.containAddonPlaceholder'),
},
},
];
}
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'keywords',
label: $t('site.group.groupName'),
},
];
}
export function useColumns(
onActionClick: OnActionClickFn<SiteApi.SiteGroup>,
): VxeTableGridOptions['columns'] {
return [
{ type: 'checkbox', width: 60 },
{
title: $t('site.group.groupId'),
field: 'group_id',
width: 80,
},
{
title: $t('site.group.groupName'),
field: 'group_name',
minWidth: 200,
},
{
title: $t('site.group.appName'),
field: 'app_list',
width: 300,
slots: {
default: ({ row }) => {
if (!row.app_list || row.app_list.length === 0) {
return <span class="text-gray-400">{$t('site.group.appListEmpty')}</span>;
}
const displayApps = row.app_list.slice(0, 4);
const remainingCount = row.app_list.length - 4;
return (
<div class="flex items-center space-x-2">
{displayApps.map((app: any) => (
<Tooltip key={app.key} title={app.title}>
<Avatar
src={app.icon}
size="small"
class="cursor-pointer"
/>
</Tooltip>
))}
{remainingCount > 0 && (
<Tooltip title={$t('site.group.moreApps', { count: remainingCount })}>
<span class="text-xs text-gray-500 cursor-pointer">
+{remainingCount}
</span>
</Tooltip>
)}
</div>
);
},
},
},
{
title: $t('site.group.addonName'),
field: 'addon_list',
width: 300,
slots: {
default: ({ row }) => {
if (!row.addon_list || row.addon_list.length === 0) {
return <span class="text-gray-400">{$t('site.group.addonListEmpty')}</span>;
}
const displayAddons = row.addon_list.slice(0, 4);
const remainingCount = row.addon_list.length - 4;
return (
<div class="flex items-center space-x-2">
{displayAddons.map((addon: any) => (
<Tooltip key={addon.key} title={addon.title}>
<Avatar
src={addon.icon}
size="small"
class="cursor-pointer"
/>
</Tooltip>
))}
{remainingCount > 0 && (
<Tooltip title={$t('site.group.moreAddons', { count: remainingCount })}>
<span class="text-xs text-gray-500 cursor-pointer">
+{remainingCount}
</span>
</Tooltip>
)}
</div>
);
},
},
},
{
title: $t('site.group.createTime'),
field: 'create_time',
width: 160,
},
{
title: $t('common.action'),
field: 'action',
width: 150,
slots: {
default: ({ row }) => (
<Space>
<Button
type="link"
size="small"
onClick={() => onActionClick('edit', row)}
>
<Icon icon="ant-design:edit-outlined" />
{$t('common.edit')}
</Button>
<Popconfirm
title={$t('site.group.deleteConfirm')}
onConfirm={() => onActionClick('delete', row)}
>
<Button type="link" size="small" danger>
<Icon icon="ant-design:delete-outlined" />
{$t('common.delete')}
</Button>
</Popconfirm>
</Space>
),
},
},
];
}

View File

@@ -0,0 +1,7 @@
<template>
<SiteGroupList />
</template>
<script lang="ts" setup>
import SiteGroupList from './list.vue';
</script>

View File

@@ -0,0 +1,99 @@
<script lang="ts" setup>
import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SiteApi } from '#/api';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Icon, Plus } from '@vben/icons';
import { Button, message, Popconfirm } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteSiteGroup, getSiteGroupList } from '#/api/core/site';
import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
schema: useGridFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getSiteGroupList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'group_id',
},
toolbarConfig: {
custom: true,
slots: {
buttons: 'toolbar-buttons',
},
},
},
});
function handleAdd() {
formDrawerApi.setData({});
formDrawerApi.open();
}
function onActionClick(actionType: string, row: SiteApi.SiteGroup) {
switch (actionType) {
case 'edit': {
formDrawerApi.setData({ groupId: row.group_id });
formDrawerApi.open();
break;
}
case 'delete': {
handleDelete(row);
break;
}
default:
break;
}
}
async function handleDelete(row: SiteApi.SiteGroup) {
try {
await deleteSiteGroup(row.group_id);
message.success($t('common.deleteSuccess'));
gridApi.reload();
} catch (error) {
message.error($t('common.deleteFailed'));
}
}
</script>
<template>
<Page auto-content-height>
<Grid>
<template #toolbar-buttons>
<Button type="primary" @click="handleAdd">
<Plus />
{{ $t('site.group.addGroup') }}
</Button>
</template>
</Grid>
<FormDrawer />
</Page>
</template>

View File

@@ -0,0 +1,204 @@
<script lang="ts" setup>
import type { SiteApi } from '#/api';
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { CheckboxGroup, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { addSiteGroup, editSiteGroup, getInstalledAddonList, getSiteGroupInfo } from '#/api/core/site';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emits = defineEmits(['success']);
const groupId = ref<number>();
const loading = ref(false);
const appList = ref<any[]>([]);
const addonList = ref<any[]>([]);
const selectedApps = ref<string[]>([]);
const [Form, formApi] = useVbenForm({
schema: useFormSchema(),
showDefaultActions: false,
});
const getDrawerTitle = computed(() => {
return groupId.value ? $t('common.edit') : $t('common.add');
});
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
const values = await formApi.getValues();
drawerApi.lock();
try {
if (groupId.value) {
await editSiteGroup(groupId.value, values);
} else {
await addSiteGroup(values);
}
// Refresh menu after successful operation
try {
await menuRefresh();
} catch (error) {
console.warn('Menu refresh failed:', error);
}
message.success($t('common.saveSuccess'));
emits('success');
drawerApi.close();
} catch (error) {
drawerApi.unlock();
}
},
async onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<{ groupId?: number }>();
groupId.value = data?.groupId;
formApi.resetForm();
await loadAddonData();
if (groupId.value) {
await loadGroupInfo();
}
}
},
});
async function loadAddonData() {
try {
const addons = await getInstalledAddonList();
appList.value = addons.filter((item: any) => item.type === 'app');
addonList.value = addons.filter((item: any) => item.type === 'addon');
// Update form schema with dynamic options
formApi.updateSchema([
{
fieldName: 'app',
componentProps: {
options: appList.value.map(item => ({
label: item.title,
value: item.key,
})),
},
},
{
fieldName: 'addon',
componentProps: {
options: addonList.value.map(item => ({
label: item.title,
value: item.key,
disabled: isAddonDisabled(item),
})),
},
},
]);
} catch (error) {
console.error('Failed to load addon data:', error);
}
}
async function loadGroupInfo() {
if (!groupId.value) return;
loading.value = true;
try {
const groupInfo = await getSiteGroupInfo(groupId.value);
formApi.setValues(groupInfo);
selectedApps.value = groupInfo.app || [];
} catch (error) {
console.error('Failed to load group info:', error);
} finally {
loading.value = false;
}
}
function isAddonDisabled(addon: any): boolean {
if (!addon.support_app || addon.support_app.length === 0) {
return false;
}
// Check if any of the supported apps are selected
const hasSupportedApp = addon.support_app.some((app: string) =>
selectedApps.value.includes(app)
);
return !hasSupportedApp;
}
function handleAppChange(selectedValues: string[]) {
selectedApps.value = selectedValues;
// Update addon disabled state
formApi.updateSchema([
{
fieldName: 'addon',
componentProps: {
options: addonList.value.map(item => ({
label: item.title,
value: item.key,
disabled: isAddonDisabled(item),
})),
},
},
]);
// Remove selected addons that are no longer supported
const currentAddonValues = formApi.getValues().addon || [];
const validAddonValues = currentAddonValues.filter((addonKey: string) => {
const addon = addonList.value.find(a => a.key === addonKey);
return addon && !isAddonDisabled(addon);
});
if (validAddonValues.length !== currentAddonValues.length) {
formApi.setValues({ addon: validAddonValues });
message.warning($t('site.group.addonRemovedDueToAppDependency'));
}
}
function handleAddonClick(addonKey: string) {
const addon = addonList.value.find(a => a.key === addonKey);
if (addon && isAddonDisabled(addon)) {
message.info($t('site.group.selectAppFirst'));
}
}
// Menu refresh function
async function menuRefresh() {
// This should call the menu refresh API
// For now, we'll just wait a bit to simulate the refresh
await new Promise(resolve => setTimeout(resolve, 1000));
}
</script>
<template>
<Drawer :title="getDrawerTitle">
<Spin :spinning="loading">
<Form>
<!-- App selection with dependency handling -->
<template #app="slotProps">
<CheckboxGroup
v-bind="slotProps"
@change="handleAppChange"
/>
</template>
<!-- Addon selection with disabled state -->
<template #addon="slotProps">
<CheckboxGroup
v-bind="slotProps"
@click="handleAddonClick"
/>
</template>
</Form>
</Spin>
</Drawer>
</template>

View File

@@ -0,0 +1,287 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SiteApi } from '#/api';
import { $t } from '#/locales';
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'site_name',
label: $t('site.list.siteName'),
rules: 'required',
},
{
component: 'Select',
fieldName: 'group_id',
label: $t('site.list.groupId'),
rules: 'required',
componentProps: {
placeholder: $t('site.list.groupIdPlaceholder'),
showSearch: true,
},
},
{
component: 'Select',
fieldName: 'uid',
label: $t('site.list.manager'),
rules: 'required',
componentProps: {
placeholder: $t('site.list.managerPlaceholder'),
showSearch: true,
allowClear: false,
},
},
{
component: 'Input',
fieldName: 'username',
label: $t('site.list.username'),
rules: 'required',
componentProps: {
placeholder: $t('site.list.usernamePlaceholder'),
},
dependencies: {
triggerFields: ['uid'],
if: ({ uid }) => uid === 0,
},
},
{
component: 'InputPassword',
fieldName: 'password',
label: $t('site.list.password'),
rules: 'required',
componentProps: {
placeholder: $t('site.list.passwordPlaceholder'),
},
dependencies: {
triggerFields: ['uid'],
if: ({ uid }) => uid === 0,
},
},
{
component: 'InputPassword',
fieldName: 'confirm_password',
label: $t('site.list.confirmPassword'),
rules: 'required|confirm:password',
componentProps: {
placeholder: $t('site.list.confirmPasswordPlaceholder'),
},
dependencies: {
triggerFields: ['uid'],
if: ({ uid }) => uid === 0,
},
},
{
component: 'Input',
fieldName: 'site_domain',
label: $t('site.list.siteDomain'),
rules: 'required',
componentProps: {
placeholder: $t('site.list.siteDomainPlaceholder'),
},
},
{
component: 'DatePicker',
fieldName: 'expire_time',
label: $t('site.list.expireTime'),
rules: 'required',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
placeholder: $t('site.list.expireTimePlaceholder'),
},
},
];
}
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'keywords',
label: $t('site.list.siteName'),
},
{
component: 'Input',
fieldName: 'site_domain',
label: $t('site.list.siteDomain'),
},
{
component: 'Select',
fieldName: 'app',
label: $t('site.list.appId'),
componentProps: {
placeholder: $t('common.selectPlaceholder'),
allowClear: true,
},
},
{
component: 'Select',
fieldName: 'group_id',
label: $t('site.list.groupId'),
componentProps: {
placeholder: $t('common.selectPlaceholder'),
allowClear: true,
},
},
{
component: 'Select',
fieldName: 'status',
label: $t('site.list.status'),
componentProps: {
placeholder: $t('common.selectPlaceholder'),
allowClear: true,
options: [
{ label: $t('common.enabled'), value: 1 },
{ label: $t('common.disabled'), value: 0 },
],
},
},
];
}
export function useColumns(
onActionClick: OnActionClickFn<SiteApi.Site>,
onStatusChange: (status: number, siteId: number) => void,
onOpenClose: (status: number, siteId: number) => void,
): VxeTableGridOptions['columns'] {
return [
{ type: 'checkbox', width: 60 },
{
title: $t('site.list.siteId'),
field: 'site_id',
width: 80,
},
{
title: $t('site.list.siteName'),
field: 'site_name',
minWidth: 200,
},
{
title: $t('site.list.siteDomain'),
field: 'site_domain',
minWidth: 200,
},
{
title: $t('site.list.appId'),
field: 'app_title',
width: 120,
},
{
title: $t('site.list.groupId'),
field: 'group_name',
width: 120,
},
{
title: $t('site.list.manager'),
field: 'username',
width: 120,
},
{
title: $t('site.list.expireTime'),
field: 'expire_time',
width: 160,
formatter: ({ cellValue }) => {
return cellValue === 0 ? $t('site.list.permanent') : cellValue;
},
},
{
title: $t('site.list.createTime'),
field: 'create_time',
width: 160,
},
{
title: $t('site.list.status'),
field: 'status',
width: 100,
slots: {
default: ({ row }) => (
<Tag
color={getStatusColor(row.status)}
class="cursor-pointer"
onClick={() => onStatusChange(row.status, row.site_id)}
>
{row.status_name}
</Tag>
),
},
},
{
title: $t('common.action'),
field: 'action',
width: 280,
slots: {
default: ({ row }) => (
<Space>
<Button
type="link"
size="small"
onClick={() => handleToSiteLink(row.site_id)}
>
<Icon icon="ant-design:login-outlined" />
{$t('site.list.toSite')}
</Button>
{(row.status === 1 || row.status === 3) && (
<Button
type="link"
size="small"
onClick={() => onOpenClose(row.status, row.site_id)}
>
{row.status === 1 ? $t('site.list.close') : $t('site.list.open')}
</Button>
)}
<Button
type="link"
size="small"
onClick={() => onActionClick('info', row)}
>
<Icon icon="ant-design:info-circle-outlined" />
{$t('site.list.info')}
</Button>
<Button
type="link"
size="small"
onClick={() => onActionClick('init', row)}
>
<Icon icon="ant-design:reload-outlined" />
{$t('site.list.initSite')}
</Button>
<Button
type="link"
size="small"
onClick={() => onActionClick('edit', row)}
>
<Icon icon="ant-design:edit-outlined" />
{$t('common.edit')}
</Button>
<Popconfirm
title={$t('site.list.deleteConfirm')}
onConfirm={() => onActionClick('delete', row)}
>
<Button type="link" size="small" danger>
<Icon icon="ant-design:delete-outlined" />
{$t('common.delete')}
</Button>
</Popconfirm>
</Space>
),
},
},
];
}
function getStatusColor(status: number): string {
const colorMap: Record<number, string> = {
0: 'red',
1: 'green',
2: 'orange',
3: 'red',
};
return colorMap[status] || 'default';
}
function handleToSiteLink(siteId: number) {
// Implementation for site switching
console.log('Switch to site:', siteId);
}

View File

@@ -0,0 +1,7 @@
<template>
<SiteList />
</template>
<script lang="ts" setup>
import SiteList from './list.vue';
</script>

View File

@@ -0,0 +1,245 @@
<script lang="ts" setup>
import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SiteApi } from '#/api';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message, Modal, Popconfirm } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
getSiteList,
modifySiteStatus,
deleteSite,
initSite,
getSiteCaptcha,
switchSite,
} from '#/api/core/site';
import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
schema: useGridFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useColumns(onActionClick, handleStatusChange, handleOpenClose),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getSiteList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'site_id',
},
toolbarConfig: {
custom: true,
slots: {
buttons: 'toolbar-buttons',
},
},
},
});
function handleAdd() {
formDrawerApi.setData({});
formDrawerApi.open();
}
function onActionClick(actionType: string, row: SiteApi.Site) {
switch (actionType) {
case 'edit': {
formDrawerApi.setData({ siteId: row.site_id });
formDrawerApi.open();
break;
}
case 'delete': {
handleDelete(row);
break;
}
case 'info': {
// Site info detail
message.info('Site info: ' + row.site_name);
break;
}
case 'init': {
handleInit(row);
break;
}
default:
break;
}
}
async function handleStatusChange(status: number, siteId: number) {
try {
await modifySiteStatus(siteId, status === 1 ? 0 : 1);
message.success($t('common.updateSuccess'));
gridApi.reload();
} catch (error) {
console.error('Failed to change status:', error);
}
}
async function handleOpenClose(status: number, siteId: number) {
try {
const newStatus = status === 1 ? 3 : 1; // 1: open -> 3: close, 3: close -> 1: open
await modifySiteStatus(siteId, newStatus);
message.success($t('common.updateSuccess'));
gridApi.reload();
} catch (error) {
console.error('Failed to open/close site:', error);
}
}
function handleDelete(row: SiteApi.Site) {
Modal.confirm({
title: $t('common.prompt'),
content: $t('site.list.deleteConfirm'),
width: 500,
onOk: async () => {
try {
// Get captcha for delete operation
const captcha = await getSiteCaptcha();
Modal.confirm({
title: $t('site.list.deleteTips'),
content: () => (
<div>
<p>{$t('site.list.deleteTips')}</p>
<div class="mt-4">
<img src={captcha.img} alt="captcha" class="mb-2" />
<input
v-model={deleteCaptchaCode}
placeholder={$t('site.list.captchaPlaceholder')}
class="ant-input"
maxlength="4"
/>
</div>
</div>
),
width: 500,
onOk: async () => {
if (!deleteCaptchaCode) {
message.error($t('site.list.captchaRequired'));
return Promise.reject();
}
try {
await deleteSite(row.site_id, deleteCaptchaCode);
message.success($t('common.deleteSuccess'));
gridApi.reload();
} catch (error) {
message.error($t('common.deleteFailed'));
throw error;
}
},
});
} catch (error) {
message.error($t('common.operationFailed'));
}
},
});
}
function handleInit(row: SiteApi.Site) {
Modal.confirm({
title: $t('site.list.initConfirm'),
content: $t('site.list.initTips'),
width: 500,
onOk: async () => {
try {
// Get captcha for init operation
const captcha = await getSiteCaptcha();
Modal.confirm({
title: $t('site.list.initTips'),
content: () => (
<div>
<p>{$t('site.list.initTips')}</p>
<div class="mt-4">
<img src={captcha.img} alt="captcha" class="mb-2" />
<input
v-model={initCaptchaCode}
placeholder={$t('site.list.captchaPlaceholder')}
class="ant-input"
maxlength="4"
/>
</div>
</div>
),
width: 500,
onOk: async () => {
if (!initCaptchaCode) {
message.error($t('site.list.captchaRequired'));
return Promise.reject();
}
try {
await initSite(row.site_id, initCaptchaCode);
message.success($t('site.list.initSuccess'));
gridApi.reload();
} catch (error) {
message.error($t('site.list.initFailed'));
throw error;
}
},
});
} catch (error) {
message.error($t('common.operationFailed'));
}
},
});
}
function handleToSiteLink(siteId: number) {
// Switch to site
Modal.confirm({
title: $t('site.list.switchSite'),
content: $t('site.list.switchSiteConfirm'),
onOk: async () => {
try {
await switchSite(siteId);
message.success($t('site.list.switchSiteSuccess'));
// Redirect to site or refresh current page
window.location.reload();
} catch (error) {
message.error($t('site.list.switchSiteFailed'));
}
},
});
}
let deleteCaptchaCode = '';
let initCaptchaCode = '';
</script>
<template>
<Page auto-content-height>
<Grid>
<template #toolbar-buttons>
<Button type="primary" @click="handleAdd">
<Plus />
{{ $t('site.list.addSite') }}
</Button>
</template>
</Grid>
<FormDrawer />
</Page>
</template>

View File

@@ -0,0 +1,210 @@
<script lang="ts" setup>
import type { SiteApi } from '#/api';
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { Avatar, Select, Space, Spin } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { Input, InputPassword, DatePicker } from 'ant-design-vue';
import {
addSite,
editSite,
getSiteGroupAll,
getSiteInfo,
} from '#/api/core/site';
import { getUserListSelect } from '#/api/core/user';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emits = defineEmits(['success']);
const siteId = ref<number>();
const groupList = ref<any[]>([]);
const userList = ref<any[]>([]);
const loading = ref(false);
const [Form, formApi] = useVbenForm({
schema: useFormSchema(),
showDefaultActions: false,
});
const getDrawerTitle = computed(() => {
return siteId.value ? $t('common.edit') : $t('common.add');
});
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
const values = await formApi.getValues();
drawerApi.lock();
try {
if (siteId.value) {
await editSite(siteId.value, values);
} else {
await addSite(values);
}
emits('success');
drawerApi.close();
} catch (error) {
drawerApi.unlock();
}
},
async onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<{ siteId?: number }>();
siteId.value = data?.siteId;
formApi.resetForm();
await loadSelectData();
if (siteId.value) {
await loadSiteInfo();
} else {
// Set default expire time to current time + 720 hours (30 days)
const defaultExpireTime = new Date();
defaultExpireTime.setHours(defaultExpireTime.getHours() + 720);
formApi.setValues({ expire_time: defaultExpireTime });
}
}
},
});
async function loadSelectData() {
try {
const [groups, users] = await Promise.all([
getSiteGroupAll(),
getUserListSelect({}),
]);
groupList.value = groups;
userList.value = [
{ uid: 0, username: $t('site.list.createNewManager'), head_img: '' },
...users,
];
// Update form schema with dynamic options
formApi.updateSchema([
{
fieldName: 'group_id',
componentProps: {
options: groupList.value.map(item => ({
label: item.group_name,
value: item.group_id,
})),
},
},
{
fieldName: 'uid',
componentProps: {
options: userList.value.map(item => ({
label: item.username,
value: item.uid,
})),
},
},
]);
} catch (error) {
console.error('Failed to load select data:', error);
}
}
async function loadSiteInfo() {
if (!siteId.value) return;
loading.value = true;
try {
const siteInfo = await getSiteInfo(siteId.value);
formApi.setValues(siteInfo);
} catch (error) {
console.error('Failed to load site info:', error);
} finally {
loading.value = false;
}
}
// Custom slot for user select with avatar
function renderUserOption(option: any) {
const user = userList.value.find(u => u.uid === option.value);
if (!user) return option.label;
return (
<Space>
<Avatar
src={user.head_img || '/static/resource/images/member_head.png'}
size="small"
/>
<span>{user.username}</span>
</Space>
);
}
// Handle user selection change
function handleUserChange(uid: number) {
// The form dependencies will handle showing/hiding new user fields
}
</script>
<template>
<Drawer :title="getDrawerTitle">
<Spin :spinning="loading">
<Form>
<!-- Site name - read only when editing -->
<template #site_name="slotProps">
<Input
v-if="!siteId"
v-bind="slotProps"
:placeholder="$t('site.list.siteNamePlaceholder')"
/>
<div v-else class="ant-input-disabled">
{{ slotProps.value }}
</div>
</template>
<!-- User select with avatar -->
<template #uid="slotProps">
<Select
v-bind="slotProps"
:placeholder="$t('site.list.managerPlaceholder')"
show-search
@change="handleUserChange"
>
<Select.Option
v-for="user in userList"
:key="user.uid"
:value="user.uid"
>
<Space>
<Avatar
:src="user.head_img || '/static/resource/images/member_head.png'"
size="small"
/>
<span>{{ user.username }}</span>
</Space>
</Select.Option>
</Select>
</template>
<!-- Site domain with tips -->
<template #site_domain="slotProps">
<div>
<Input
v-bind="slotProps"
:placeholder="$t('site.list.siteDomainPlaceholder')"
/>
<div class="text-xs text-gray-500 mt-1">
<p>{{ $t('site.list.siteDomainTips') }}</p>
<p>{{ $t('site.list.siteDomainTipsTwo') }}</p>
<p>{{ $t('site.list.siteDomainTipsThree') }}</p>
</div>
</div>
</template>
</Form>
</Spin>
</Drawer>
</template>