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:
181
admin-vben/apps/web-antd/src/views/site/group/data.ts
Normal file
181
admin-vben/apps/web-antd/src/views/site/group/data.ts
Normal 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>
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
7
admin-vben/apps/web-antd/src/views/site/group/index.vue
Normal file
7
admin-vben/apps/web-antd/src/views/site/group/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<SiteGroupList />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SiteGroupList from './list.vue';
|
||||
</script>
|
||||
99
admin-vben/apps/web-antd/src/views/site/group/list.vue
Normal file
99
admin-vben/apps/web-antd/src/views/site/group/list.vue
Normal 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>
|
||||
204
admin-vben/apps/web-antd/src/views/site/group/modules/form.vue
Normal file
204
admin-vben/apps/web-antd/src/views/site/group/modules/form.vue
Normal 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>
|
||||
287
admin-vben/apps/web-antd/src/views/site/list/data.ts
Normal file
287
admin-vben/apps/web-antd/src/views/site/list/data.ts
Normal 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);
|
||||
}
|
||||
7
admin-vben/apps/web-antd/src/views/site/list/index.vue
Normal file
7
admin-vben/apps/web-antd/src/views/site/list/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<SiteList />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SiteList from './list.vue';
|
||||
</script>
|
||||
245
admin-vben/apps/web-antd/src/views/site/list/list.vue
Normal file
245
admin-vben/apps/web-antd/src/views/site/list/list.vue
Normal 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>
|
||||
210
admin-vben/apps/web-antd/src/views/site/list/modules/form.vue
Normal file
210
admin-vben/apps/web-antd/src/views/site/list/modules/form.vue
Normal 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>
|
||||
Reference in New Issue
Block a user