chore: push latest changes
This commit is contained in:
@@ -93,6 +93,41 @@ export const getCustomMenuApi = () => {
|
||||
return request.get('/adminapi/wechat/menu');
|
||||
};
|
||||
|
||||
// 获取菜单列表
|
||||
export const getMenuListApi = (params: { page?: number; limit?: number }) => {
|
||||
return request.get('/adminapi/wechat/menu', { params });
|
||||
};
|
||||
|
||||
// 获取菜单详情
|
||||
export const getWechatMenuInfo = (id: number) => {
|
||||
return request.get(`/adminapi/wechat/menu/${id}`);
|
||||
};
|
||||
|
||||
// 创建菜单
|
||||
export const createWechatMenu = (data: any) => {
|
||||
return request.post('/adminapi/wechat/menu', data);
|
||||
};
|
||||
|
||||
// 更新菜单
|
||||
export const updateWechatMenu = (data: any) => {
|
||||
return request.put('/adminapi/wechat/menu', data);
|
||||
};
|
||||
|
||||
// 删除菜单
|
||||
export const deleteWechatMenu = (id: number) => {
|
||||
return request.delete(`/adminapi/wechat/menu/${id}`);
|
||||
};
|
||||
|
||||
// 同步菜单
|
||||
export const syncWechatMenu = () => {
|
||||
return request.post('/adminapi/wechat/menu/sync');
|
||||
};
|
||||
|
||||
// 发布菜单
|
||||
export const publishWechatMenu = () => {
|
||||
return request.post('/adminapi/wechat/menu/publish');
|
||||
};
|
||||
|
||||
// 保存自定义菜单
|
||||
export const saveCustomMenuApi = (data: {
|
||||
button: Array<{
|
||||
@@ -147,10 +182,29 @@ export const getUserListApi = (params: { page?: number; limit?: number; nickname
|
||||
};
|
||||
|
||||
// 同步用户
|
||||
export const syncUserApi = () => {
|
||||
export const syncWechatUser = () => {
|
||||
return request.post('/adminapi/wechat/user/sync');
|
||||
};
|
||||
|
||||
// 导出用户
|
||||
export const exportWechatUser = () => {
|
||||
return request.post('/adminapi/wechat/user/export');
|
||||
};
|
||||
|
||||
// 更新用户信息
|
||||
export const updateWechatUser = (data: {
|
||||
openid: string;
|
||||
remark?: string;
|
||||
groupid?: number;
|
||||
}) => {
|
||||
return request.put('/adminapi/wechat/user', data);
|
||||
};
|
||||
|
||||
// 获取用户信息
|
||||
export const getWechatUserInfo = (openid: string) => {
|
||||
return request.get(`/adminapi/wechat/user/${openid}`);
|
||||
};
|
||||
|
||||
// 获取用户详情
|
||||
export const getUserDetailApi = (openid: string) => {
|
||||
return request.get(`/adminapi/wechat/user/${openid}`);
|
||||
@@ -161,16 +215,26 @@ export const getMaterialListApi = (params: { page?: number; limit?: number; type
|
||||
return request.get('/adminapi/wechat/material', { params });
|
||||
};
|
||||
|
||||
// 获取素材详情
|
||||
export const getWechatMaterialInfo = (id: number) => {
|
||||
return request.get(`/adminapi/wechat/material/${id}`);
|
||||
};
|
||||
|
||||
// 同步素材
|
||||
export const syncMaterialApi = () => {
|
||||
export const syncWechatMaterial = () => {
|
||||
return request.post('/adminapi/wechat/material/sync');
|
||||
};
|
||||
|
||||
// 上传素材
|
||||
export const uploadMaterialApi = (data: FormData) => {
|
||||
export const uploadWechatMaterial = (data: FormData) => {
|
||||
return request.post('/adminapi/wechat/material/upload', data);
|
||||
};
|
||||
|
||||
// 更新素材
|
||||
export const updateWechatMaterial = (data: any) => {
|
||||
return request.put('/adminapi/wechat/material', data);
|
||||
};
|
||||
|
||||
// 删除素材
|
||||
export const deleteMaterialApi = (id: number) => {
|
||||
return request.delete(`/adminapi/wechat/material/${id}`);
|
||||
|
||||
@@ -18,6 +18,45 @@
|
||||
"appList": "应用列表",
|
||||
"chooseLayout": "选择布局"
|
||||
},
|
||||
"menu": {
|
||||
"auth": "权限管理",
|
||||
"user": "用户管理",
|
||||
"role": "角色管理",
|
||||
"menu": "菜单管理",
|
||||
"site": "站点管理",
|
||||
"siteGroup": "站点分组",
|
||||
"diy": "DIY装修",
|
||||
"channel": {
|
||||
"weapp": "微信小程序",
|
||||
"wechat": {
|
||||
"access": "接入指引",
|
||||
"config": "配置管理",
|
||||
"template": "模板消息",
|
||||
"menu": "自定义菜单",
|
||||
"user": "用户管理",
|
||||
"material": "素材管理",
|
||||
"tutorial": "使用教程"
|
||||
}
|
||||
},
|
||||
"setting": {
|
||||
"system": "系统设置",
|
||||
"payment": "支付设置",
|
||||
"sms": "短信设置",
|
||||
"storage": "存储设置"
|
||||
},
|
||||
"app": {
|
||||
"list": "应用管理"
|
||||
},
|
||||
"tools": {
|
||||
"backup": "数据备份"
|
||||
},
|
||||
"finance": {
|
||||
"payment": "支付记录"
|
||||
},
|
||||
"log": {
|
||||
"admin": "管理员日志"
|
||||
}
|
||||
},
|
||||
"channel": {
|
||||
"weapp": {
|
||||
"title": "微信小程序",
|
||||
@@ -56,4 +95,4 @@
|
||||
"list": "存储配置"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
218
admin-vben/apps/web-antd/src/router/routes/modules/admin.ts
Normal file
218
admin-vben/apps/web-antd/src/router/routes/modules/admin.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/auth',
|
||||
name: 'Auth',
|
||||
component: () => import('#/views/auth/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.auth'),
|
||||
icon: 'mdi:account-key',
|
||||
permissions: ['auth.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
name: 'User',
|
||||
component: () => import('#/views/user/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.user'),
|
||||
icon: 'mdi:account-group',
|
||||
permissions: ['user.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/role',
|
||||
name: 'Role',
|
||||
component: () => import('#/views/role/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.role'),
|
||||
icon: 'mdi:shield-account',
|
||||
permissions: ['role.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/menu',
|
||||
name: 'Menu',
|
||||
component: () => import('#/views/menu/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.menu'),
|
||||
icon: 'mdi:menu',
|
||||
permissions: ['menu.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/site',
|
||||
name: 'Site',
|
||||
component: () => import('#/views/site/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.site'),
|
||||
icon: 'mdi:web',
|
||||
permissions: ['site.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/site-group',
|
||||
name: 'SiteGroup',
|
||||
component: () => import('#/views/site/group.vue'),
|
||||
meta: {
|
||||
title: $t('menu.siteGroup'),
|
||||
icon: 'mdi:folder-multiple',
|
||||
permissions: ['site.group.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/diy',
|
||||
name: 'Diy',
|
||||
component: () => import('#/views/diy/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.diy'),
|
||||
icon: 'mdi:palette',
|
||||
permissions: ['diy.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/channel/weapp',
|
||||
name: 'ChannelWeapp',
|
||||
component: () => import('#/views/channel/weapp/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.channel.weapp'),
|
||||
icon: 'mdi:wechat',
|
||||
permissions: ['channel.weapp.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/channel/wechat/access',
|
||||
name: 'ChannelWechatAccess',
|
||||
component: () => import('#/views/channel/wechat/access/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.channel.wechat.access'),
|
||||
icon: 'mdi:account-check',
|
||||
permissions: ['channel.wechat.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/channel/wechat/config',
|
||||
name: 'ChannelWechatConfig',
|
||||
component: () => import('#/views/channel/wechat/config/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.channel.wechat.config'),
|
||||
icon: 'mdi:cog',
|
||||
permissions: ['channel.wechat.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/channel/wechat/template',
|
||||
name: 'ChannelWechatTemplate',
|
||||
component: () => import('#/views/channel/wechat/template/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.channel.wechat.template'),
|
||||
icon: 'mdi:file-document',
|
||||
permissions: ['channel.wechat.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/channel/wechat/version',
|
||||
name: 'ChannelWechatVersion',
|
||||
component: () => import('#/views/channel/wechat/version/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.channel.wechat.version'),
|
||||
icon: 'mdi:tag',
|
||||
permissions: ['channel.wechat.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/channel/wechat/tutorial',
|
||||
name: 'ChannelWechatTutorial',
|
||||
component: () => import('#/views/channel/wechat/tutorial/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.channel.wechat.tutorial'),
|
||||
icon: 'mdi:book',
|
||||
permissions: ['channel.wechat.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/channel/wechat/menu',
|
||||
name: 'ChannelWechatMenu',
|
||||
component: () => import('#/views/channel/wechat/menu/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.channel.wechat.menu'),
|
||||
icon: 'mdi:menu',
|
||||
permissions: ['channel.wechat.menu.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/channel/wechat/user',
|
||||
name: 'ChannelWechatUser',
|
||||
component: () => import('#/views/channel/wechat/user/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.channel.wechat.user'),
|
||||
icon: 'mdi:account-multiple',
|
||||
permissions: ['channel.wechat.user.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/channel/wechat/material',
|
||||
name: 'ChannelWechatMaterial',
|
||||
component: () => import('#/views/channel/wechat/material/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.channel.wechat.material'),
|
||||
icon: 'mdi:folder-image',
|
||||
permissions: ['channel.wechat.material.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/setting/system',
|
||||
name: 'SettingSystem',
|
||||
component: () => import('#/views/setting/system/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.setting.system'),
|
||||
icon: 'mdi:cog',
|
||||
permissions: ['setting.system.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/app/list',
|
||||
name: 'AppList',
|
||||
component: () => import('#/views/app/list/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.app.list'),
|
||||
icon: 'mdi:apps',
|
||||
permissions: ['app.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tools/backup',
|
||||
name: 'ToolsBackup',
|
||||
component: () => import('#/views/tools/backup/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.tools.backup'),
|
||||
icon: 'mdi:backup',
|
||||
permissions: ['tools.backup.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/finance/payment',
|
||||
name: 'FinancePayment',
|
||||
component: () => import('#/views/finance/payment/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.finance.payment'),
|
||||
icon: 'mdi:cash-multiple',
|
||||
permissions: ['finance.payment.manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/log/admin',
|
||||
name: 'LogAdmin',
|
||||
component: () => import('#/views/log/admin/list.vue'),
|
||||
meta: {
|
||||
title: $t('menu.log.admin'),
|
||||
icon: 'mdi:file-document',
|
||||
permissions: ['log.admin.manage'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
135
admin-vben/apps/web-antd/src/views/app/list/data.ts
Normal file
135
admin-vben/apps/web-antd/src/views/app/list/data.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { VxeGridProps } from '@vben/plugins/vxe-table';
|
||||
|
||||
export interface AppInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
author: string;
|
||||
version: string;
|
||||
icon: string;
|
||||
cover: string;
|
||||
preview: string;
|
||||
path: string;
|
||||
admin_path: string;
|
||||
type: 'addon' | 'module' | 'plugin';
|
||||
category: string;
|
||||
tags: string;
|
||||
require: string;
|
||||
install: 0 | 1;
|
||||
status: 0 | 1;
|
||||
config: string;
|
||||
hooks: string;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
export interface AppForm {
|
||||
id?: number;
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
author: string;
|
||||
version: string;
|
||||
icon: string;
|
||||
cover: string;
|
||||
preview: string;
|
||||
path: string;
|
||||
admin_path: string;
|
||||
type: string;
|
||||
category: string;
|
||||
tags: string;
|
||||
require: string;
|
||||
install: 0 | 1;
|
||||
status: 0 | 1;
|
||||
config: string;
|
||||
hooks: string;
|
||||
}
|
||||
|
||||
export const typeOptions = [
|
||||
{ label: '插件', value: 'addon' },
|
||||
{ label: '模块', value: 'module' },
|
||||
{ label: '应用', value: 'plugin' },
|
||||
];
|
||||
|
||||
export const categoryOptions = [
|
||||
{ label: '系统工具', value: 'system' },
|
||||
{ label: '营销工具', value: 'marketing' },
|
||||
{ label: '支付工具', value: 'payment' },
|
||||
{ label: '物流工具', value: 'logistics' },
|
||||
{ label: '客服工具', value: 'service' },
|
||||
{ label: '数据分析', value: 'analytics' },
|
||||
{ label: '其他', value: 'other' },
|
||||
];
|
||||
|
||||
export const statusOptions = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
];
|
||||
|
||||
export const installOptions = [
|
||||
{ label: '已安装', value: 1 },
|
||||
{ label: '未安装', value: 0 },
|
||||
];
|
||||
|
||||
export const gridOptions: VxeGridProps<AppInfo> = {
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'icon', title: '图标', width: 80, formatter: ({ cellValue }) => {
|
||||
return cellValue ? `<i class="${cellValue}" style="font-size: 24px;"></i>` : '';
|
||||
} },
|
||||
{ field: 'title', title: '应用名称', minWidth: 150 },
|
||||
{ field: 'name', title: '应用标识', minWidth: 120 },
|
||||
{ field: 'version', title: '版本', width: 100 },
|
||||
{ field: 'author', title: '作者', width: 120 },
|
||||
{ field: 'type', title: '类型', width: 100, formatter: ({ cellValue }) => {
|
||||
const option = typeOptions.find(item => item.value === cellValue);
|
||||
return option?.label || cellValue;
|
||||
}},
|
||||
{ field: 'category', title: '分类', width: 100, formatter: ({ cellValue }) => {
|
||||
const option = categoryOptions.find(item => item.value === cellValue);
|
||||
return option?.label || cellValue;
|
||||
}},
|
||||
{ field: 'install', title: '安装状态', width: 100, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '已安装' : '未安装';
|
||||
}},
|
||||
{ field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '启用' : '禁用';
|
||||
}},
|
||||
{ field: 'create_time', title: '创建时间', width: 180 },
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: AppInfo) => {
|
||||
// This will be handled in the component
|
||||
},
|
||||
options: [
|
||||
{ code: 'install', text: '安装', icon: 'ant-design:download-outlined' },
|
||||
{ code: 'config', text: '配置', icon: 'ant-design:setting-outlined' },
|
||||
{ code: 'uninstall', text: '卸载', icon: 'ant-design:delete-outlined', danger: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
print: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
},
|
||||
};
|
||||
269
admin-vben/apps/web-antd/src/views/app/list/list.vue
Normal file
269
admin-vben/apps/web-antd/src/views/app/list/list.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<VbenVxeGrid
|
||||
ref="gridRef"
|
||||
:grid-options="gridOptions"
|
||||
:query-form-schema="queryFormSchema"
|
||||
@toolbar-button-click="handleToolbarButtonClick"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<VbenButton type="primary" @click="handleInstallFromStore">
|
||||
<SvgIcon icon="mdi:download" class="mr-1" />
|
||||
应用商店
|
||||
</VbenButton>
|
||||
</template>
|
||||
|
||||
<template #icon="{ row }">
|
||||
<img
|
||||
v-if="row.icon"
|
||||
:src="row.icon"
|
||||
alt="应用图标"
|
||||
class="w-10 h-10 rounded object-cover"
|
||||
@error="(e: any) => e.target.src = 'https://via.placeholder.com/40x40'"
|
||||
/>
|
||||
<div v-else class="w-10 h-10 bg-gray-200 rounded flex items-center justify-center">
|
||||
<SvgIcon icon="mdi:application" class="text-gray-400" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<VbenButton
|
||||
v-if="row.install === 0"
|
||||
size="small"
|
||||
type="primary"
|
||||
variant="text"
|
||||
@click="handleInstall(row)"
|
||||
>
|
||||
安装
|
||||
</VbenButton>
|
||||
<template v-else>
|
||||
<VbenButton
|
||||
size="small"
|
||||
type="primary"
|
||||
variant="text"
|
||||
@click="handleConfig(row)"
|
||||
>
|
||||
配置
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
size="small"
|
||||
type="primary"
|
||||
variant="text"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
{{ $t('common.edit') }}
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
size="small"
|
||||
type="warning"
|
||||
variant="text"
|
||||
@click="handleUpdate(row)"
|
||||
>
|
||||
更新
|
||||
</VbenButton>
|
||||
<VbenPopconfirm
|
||||
title="确定卸载该应用吗?"
|
||||
@confirm="handleUninstall(row)"
|
||||
>
|
||||
<VbenButton
|
||||
size="small"
|
||||
type="danger"
|
||||
variant="text"
|
||||
>
|
||||
卸载
|
||||
</VbenButton>
|
||||
</VbenPopconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</VbenVxeGrid>
|
||||
|
||||
<AppFormModal
|
||||
v-model:visible="modalVisible"
|
||||
:id="editingId"
|
||||
@cancel="handleModalCancel"
|
||||
@submit="handleModalSubmit"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { AppInfo, AppForm } from './data';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useVbenVxeGrid, VbenButton, VbenPopconfirm, VbenVxeGrid } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { getAppListApi, installAppApi, uninstallAppApi, updateAppApi, getAppStoreListApi } from '#/api/core/app';
|
||||
import { SvgIcon } from '#/components/icon';
|
||||
|
||||
import AppFormModal from './modules/form.vue';
|
||||
import { gridOptions } from './data';
|
||||
|
||||
const router = useRouter();
|
||||
const gridRef = ref();
|
||||
const modalVisible = ref(false);
|
||||
const editingId = ref<number | undefined>();
|
||||
|
||||
const queryFormSchema = computed(() => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '应用标识',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'title',
|
||||
label: '应用名称',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'type',
|
||||
label: '应用类型',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '插件', value: 'addon' },
|
||||
{ label: '模块', value: 'module' },
|
||||
{ label: '应用', value: 'plugin' },
|
||||
],
|
||||
placeholder: '请选择应用类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'install',
|
||||
label: '安装状态',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '已安装', value: 1 },
|
||||
{ label: '未安装', value: 0 },
|
||||
],
|
||||
placeholder: '请选择安装状态',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
],
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
queryFormSchema,
|
||||
});
|
||||
|
||||
function handleToolbarButtonClick(event: string) {
|
||||
switch (event) {
|
||||
case 'add':
|
||||
handleAdd();
|
||||
break;
|
||||
case 'refresh':
|
||||
handleRefresh();
|
||||
break;
|
||||
case 'export':
|
||||
handleExport();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
editingId.value = undefined;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleInstallFromStore() {
|
||||
// Navigate to app store
|
||||
router.push({ name: 'AppStore' });
|
||||
}
|
||||
|
||||
function handleConfig(row: AppInfo) {
|
||||
// Navigate to app config page
|
||||
router.push({
|
||||
name: 'AppConfig',
|
||||
params: { id: row.id },
|
||||
query: { name: row.name }
|
||||
});
|
||||
}
|
||||
|
||||
function handleEdit(row: AppInfo) {
|
||||
editingId.value = row.id;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleInstall(row: AppInfo) {
|
||||
try {
|
||||
await installAppApi(row.id);
|
||||
await handleRefresh();
|
||||
$message.success('安装成功');
|
||||
} catch (error) {
|
||||
$message.error('安装失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUninstall(row: AppInfo) {
|
||||
try {
|
||||
await uninstallAppApi(row.id);
|
||||
await handleRefresh();
|
||||
$message.success('卸载成功');
|
||||
} catch (error) {
|
||||
$message.error('卸载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdate(row: AppInfo) {
|
||||
try {
|
||||
await updateAppApi(row.id);
|
||||
await handleRefresh();
|
||||
$message.success('更新成功');
|
||||
} catch (error) {
|
||||
$message.error('更新失败');
|
||||
}
|
||||
}
|
||||
|
||||
function handleModalCancel() {
|
||||
modalVisible.value = false;
|
||||
editingId.value = undefined;
|
||||
}
|
||||
|
||||
async function handleModalSubmit(data: AppForm) {
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await updateAppApi(editingId.value, data);
|
||||
$message.success('更新成功');
|
||||
} else {
|
||||
// Create new app would be handled by app store
|
||||
$message.info('请通过应用商店安装应用');
|
||||
}
|
||||
modalVisible.value = false;
|
||||
await handleRefresh();
|
||||
} catch (error) {
|
||||
$message.error(editingId.value ? '更新失败' : '创建失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRefresh() {
|
||||
await gridApi.query();
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
gridApi.exportData({
|
||||
filename: '应用列表',
|
||||
type: 'csv',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
97
admin-vben/apps/web-antd/src/views/app/list/modules/form.vue
Normal file
97
admin-vben/apps/web-antd/src/views/app/list/modules/form.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div>
|
||||
<VbenForm
|
||||
:handle-submit="handleSubmit"
|
||||
:model="model"
|
||||
:schema="formSchemas"
|
||||
:show-default-actions="false"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<template #form-submit>
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<VbenButton @click="handleCancel" variant="outline">
|
||||
{{ $t('common.cancel') }}
|
||||
</VbenButton>
|
||||
<VbenButton type="primary" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { AppForm } from '../data';
|
||||
|
||||
import { VbenButton, VbenForm, useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { useAppFormSchemas } from './formSchemas';
|
||||
|
||||
interface Props {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', data: AppForm): void;
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
id: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const [Drawer] = useVbenDrawer();
|
||||
const model = ref<AppForm>({
|
||||
name: '',
|
||||
title: '',
|
||||
description: '',
|
||||
author: '',
|
||||
version: '1.0.0',
|
||||
icon: '',
|
||||
cover: '',
|
||||
preview: '',
|
||||
path: '',
|
||||
admin_path: '',
|
||||
type: 'addon',
|
||||
category: 'other',
|
||||
tags: '',
|
||||
require: '',
|
||||
install: 0,
|
||||
status: 1,
|
||||
config: '',
|
||||
hooks: '',
|
||||
});
|
||||
|
||||
const formSchemas = useAppFormSchemas();
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await Drawer?.formApi.validate();
|
||||
const formValues = Drawer?.formApi.getValues() || model.value;
|
||||
emit('submit', formValues);
|
||||
} catch (error) {
|
||||
console.error('Form validation failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
// Load app data if editing
|
||||
onMounted(async () => {
|
||||
if (props.id) {
|
||||
try {
|
||||
// Load app data
|
||||
const appData = await getAppDetailApi(props.id);
|
||||
model.value = { ...appData };
|
||||
} catch (error) {
|
||||
console.error('Failed to load app data:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,169 @@
|
||||
import type { AppForm } from '../data';
|
||||
|
||||
import { useVbenForm } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { typeOptions, categoryOptions, statusOptions } from '../data';
|
||||
|
||||
export const useAppFormSchemas = () => {
|
||||
const formSchemas = computed(() => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '应用标识',
|
||||
rules: 'required|pattern:^[a-zA-Z][a-zA-Z0-9_]*$',
|
||||
componentProps: {
|
||||
placeholder: '请输入应用标识(英文)',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'title',
|
||||
label: '应用名称',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入应用名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'description',
|
||||
label: '应用描述',
|
||||
componentProps: {
|
||||
placeholder: '请输入应用描述',
|
||||
rows: 3,
|
||||
maxlength: 500,
|
||||
showCount: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'author',
|
||||
label: '作者',
|
||||
componentProps: {
|
||||
placeholder: '请输入作者名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'version',
|
||||
label: '版本号',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入版本号,如:1.0.0',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Upload',
|
||||
fieldName: 'icon',
|
||||
label: '应用图标',
|
||||
componentProps: {
|
||||
accept: 'image/*',
|
||||
maxCount: 1,
|
||||
showUploadList: true,
|
||||
listType: 'picture-card',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Upload',
|
||||
fieldName: 'cover',
|
||||
label: '应用封面',
|
||||
componentProps: {
|
||||
accept: 'image/*',
|
||||
maxCount: 1,
|
||||
showUploadList: true,
|
||||
listType: 'picture-card',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'preview',
|
||||
label: '预览图',
|
||||
componentProps: {
|
||||
placeholder: '请输入预览图URL',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'path',
|
||||
label: '前台路径',
|
||||
componentProps: {
|
||||
placeholder: '请输入前台访问路径',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'admin_path',
|
||||
label: '后台路径',
|
||||
componentProps: {
|
||||
placeholder: '请输入后台管理路径',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'type',
|
||||
label: '应用类型',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: typeOptions,
|
||||
placeholder: '请选择应用类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'category',
|
||||
label: '应用分类',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: categoryOptions,
|
||||
placeholder: '请选择应用分类',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'tags',
|
||||
label: '应用标签',
|
||||
componentProps: {
|
||||
placeholder: '请输入应用标签,多个用逗号分隔',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'require',
|
||||
label: '依赖要求',
|
||||
componentProps: {
|
||||
placeholder: '请输入依赖要求,如:PHP>=7.2, MySQL>=5.7',
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'hooks',
|
||||
label: '钩子配置',
|
||||
componentProps: {
|
||||
placeholder: '请输入钩子配置(JSON格式)',
|
||||
rows: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'config',
|
||||
label: '配置信息',
|
||||
componentProps: {
|
||||
placeholder: '请输入配置信息(JSON格式)',
|
||||
rows: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
defaultValue: 1,
|
||||
componentProps: {
|
||||
options: statusOptions,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return formSchemas;
|
||||
};
|
||||
@@ -1,135 +1,92 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { VxeGridProps } from '@vben/plugins/vxe-table';
|
||||
|
||||
export interface MaterialItem {
|
||||
export interface WechatMaterial {
|
||||
id: number;
|
||||
media_id: string;
|
||||
type: 'image' | 'voice' | 'video' | 'news';
|
||||
type: 'image' | 'voice' | 'video' | 'news' | 'thumb';
|
||||
title?: string;
|
||||
introduction?: string;
|
||||
url: string;
|
||||
thumb_url?: string;
|
||||
content?: string;
|
||||
digest?: string;
|
||||
show_cover_pic: 0 | 1;
|
||||
author?: string;
|
||||
content_source_url?: string;
|
||||
local_url?: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
duration?: number;
|
||||
news_item?: NewsItem[];
|
||||
status: 0 | 1;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
export interface NewsItem {
|
||||
title: string;
|
||||
author: string;
|
||||
digest: string;
|
||||
show_cover_pic: 0 | 1;
|
||||
content: string;
|
||||
content_source_url: string;
|
||||
thumb_media_id: string;
|
||||
thumb_url: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface MaterialForm {
|
||||
id?: number;
|
||||
type: 'image' | 'voice' | 'video' | 'news';
|
||||
type: string;
|
||||
title?: string;
|
||||
introduction?: string;
|
||||
file?: File;
|
||||
news_item?: NewsItem[];
|
||||
status: 0 | 1;
|
||||
url?: string;
|
||||
thumb_url?: string;
|
||||
content?: string;
|
||||
digest?: string;
|
||||
show_cover_pic: 0 | 1;
|
||||
author?: string;
|
||||
content_source_url?: string;
|
||||
}
|
||||
|
||||
export const materialTypeOptions = [
|
||||
export const typeOptions = [
|
||||
{ label: '图片', value: 'image' },
|
||||
{ label: '语音', value: 'voice' },
|
||||
{ label: '视频', value: 'video' },
|
||||
{ label: '图文', value: 'news' },
|
||||
{ label: '缩略图', value: 'thumb' },
|
||||
];
|
||||
|
||||
export const materialTypeMap = {
|
||||
image: '图片',
|
||||
voice: '语音',
|
||||
video: '视频',
|
||||
news: '图文',
|
||||
};
|
||||
|
||||
export const statusOptions = [
|
||||
{ label: '正常', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
export const showCoverOptions = [
|
||||
{ label: '不显示', value: 0 },
|
||||
{ label: '显示', value: 1 },
|
||||
];
|
||||
|
||||
export const statusMap = {
|
||||
1: '正常',
|
||||
0: '禁用',
|
||||
};
|
||||
|
||||
export const querySchema = [
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '素材类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: materialTypeOptions,
|
||||
placeholder: '请选择素材类型',
|
||||
export const gridOptions: VxeGridProps<WechatMaterial> = {
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'media_id', title: 'MediaID', width: 180 },
|
||||
{ field: 'type', title: '类型', width: 100, formatter: ({ cellValue }) => {
|
||||
const option = typeOptions.find(item => item.value === cellValue);
|
||||
return option?.label || cellValue;
|
||||
}},
|
||||
{ field: 'title', title: '标题', minWidth: 150 },
|
||||
{ field: 'url', title: 'URL', minWidth: 200, showOverflow: true },
|
||||
{ field: 'thumb_url', title: '缩略图', width: 120, formatter: ({ cellValue }) => {
|
||||
return cellValue ? `<img src="${cellValue}" style="width: 60px; height: 60px; object-fit: cover;" />` : '';
|
||||
} },
|
||||
{ field: 'create_time', title: '创建时间', width: 180 },
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: WechatMaterial) => {
|
||||
// This will be handled in the component
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
},
|
||||
{
|
||||
fieldName: 'title',
|
||||
label: '标题',
|
||||
component: 'Input',
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
print: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: statusOptions,
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'create_time',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
type: 'datetimerange',
|
||||
rangeSeparator: '至',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{
|
||||
field: 'thumb_url',
|
||||
title: '缩略图',
|
||||
width: 100,
|
||||
slots: { default: 'thumb' },
|
||||
align: 'center',
|
||||
},
|
||||
{ field: 'title', title: '标题', minWidth: 150 },
|
||||
{ field: 'type', title: '类型', width: 100, slots: { default: 'type' } },
|
||||
{ field: 'filename', title: '文件名', minWidth: 200 },
|
||||
{ field: 'size', title: '大小', width: 120, slots: { default: 'size' } },
|
||||
{ field: 'width', title: '宽度', width: 100 },
|
||||
{ field: 'height', title: '高度', width: 100 },
|
||||
{ field: 'duration', title: '时长', width: 100, slots: { default: 'duration' } },
|
||||
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } },
|
||||
{ field: 'create_time', title: '创建时间', width: 180 },
|
||||
{ field: 'update_time', title: '更新时间', width: 180 },
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,235 +1,123 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="m-4">
|
||||
<VbenVxeGrid
|
||||
ref="gridRef"
|
||||
:form-options="formOptions"
|
||||
:grid-options="gridOptions"
|
||||
:grid-events="gridEvents"
|
||||
:query-schema="querySchema"
|
||||
title="微信素材管理"
|
||||
@toolbar-button-click="handleToolbarClick"
|
||||
@cell-operation-click="handleCellOperationClick"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<template #toolbar-buttons>
|
||||
<VbenButton type="primary" @click="handleAdd">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
上传素材
|
||||
</VbenButton>
|
||||
<VbenButton type="success" @click="handleSync">
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
<VbenButton type="default" @click="handleSync">
|
||||
<template #icon>
|
||||
<SyncOutlined />
|
||||
</template>
|
||||
同步素材
|
||||
</VbenButton>
|
||||
</template>
|
||||
|
||||
<template #thumb="{ row }">
|
||||
<div class="flex justify-center">
|
||||
<img
|
||||
v-if="row.thumb_url"
|
||||
:src="row.thumb_url"
|
||||
:alt="row.title"
|
||||
class="w-16 h-16 object-cover rounded"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="w-16 h-16 bg-gray-100 rounded flex items-center justify-center text-gray-400"
|
||||
>
|
||||
<FileText class="w-8 h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #type="{ row }">
|
||||
<VbenTag :type="getTypeColor(row.type)">
|
||||
{{ materialTypeMap[row.type] }}
|
||||
</VbenTag>
|
||||
</template>
|
||||
|
||||
<template #size="{ row }">
|
||||
{{ formatFileSize(row.size) }}
|
||||
</template>
|
||||
|
||||
<template #duration="{ row }">
|
||||
{{ row.duration ? formatDuration(row.duration) : '-' }}
|
||||
</template>
|
||||
|
||||
<template #status="{ row }">
|
||||
<VbenTag :type="row.status === 1 ? 'success' : 'error'">
|
||||
{{ statusMap[row.status] }}
|
||||
</VbenTag>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<VbenButton
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleView(row)"
|
||||
>
|
||||
查看
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</VbenButton>
|
||||
<VbenPopconfirm
|
||||
title="确定删除该素材吗?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<VbenButton type="text" size="small" danger>
|
||||
删除
|
||||
</VbenButton>
|
||||
</VbenPopconfirm>
|
||||
</template>
|
||||
</VbenVxeGrid>
|
||||
|
||||
<MaterialUploadModal
|
||||
v-model="uploadModalVisible"
|
||||
@reload="reloadTable"
|
||||
/>
|
||||
|
||||
<MaterialEditModal
|
||||
v-model="editModalVisible"
|
||||
:data="currentData"
|
||||
@reload="reloadTable"
|
||||
/>
|
||||
|
||||
<MaterialViewModal
|
||||
v-model="viewModalVisible"
|
||||
:data="currentData"
|
||||
<MaterialForm
|
||||
v-model="drawerVisible"
|
||||
:material="currentMaterial"
|
||||
@success="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { Plus, RefreshCw, FileText } from '@vben/icons';
|
||||
import { ref } from 'vue';
|
||||
import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
import { VbenButton } from '@vben/common-ui';
|
||||
import { PlusOutlined, SyncOutlined } from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import MaterialForm from './modules/material-form.vue';
|
||||
import { gridOptions, querySchema } from './data';
|
||||
import { getWechatMaterialList, syncWechatMaterial, deleteWechatMaterial } from '#/api/core/wechat';
|
||||
import type { WechatMaterial } from './data';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { VbenButton, VbenMessage, VbenPopconfirm, VbenTag } from '@vben/common-ui';
|
||||
const drawerVisible = ref(false);
|
||||
const currentMaterial = ref<WechatMaterial | null>(null);
|
||||
|
||||
import { getWechatMaterialList, deleteWechatMaterial, syncWechatMaterial } from '#/api/core/wechat';
|
||||
import MaterialUploadModal from './modules/upload-modal.vue';
|
||||
import MaterialEditModal from './modules/edit-modal.vue';
|
||||
import MaterialViewModal from './modules/view-modal.vue';
|
||||
|
||||
import type { MaterialItem } from './data';
|
||||
import { columns, querySchema, materialTypeMap, statusMap } from './data';
|
||||
|
||||
const uploadModalVisible = ref(false);
|
||||
const editModalVisible = ref(false);
|
||||
const viewModalVisible = ref(false);
|
||||
const currentData = ref<MaterialItem | null>(null);
|
||||
|
||||
const gridRef = ref();
|
||||
|
||||
const formOptions = computed(() => ({
|
||||
schema: querySchema,
|
||||
showCollapseButton: true,
|
||||
fieldSize: 'medium',
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
}));
|
||||
|
||||
const gridOptions = computed(() => ({
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
const [VbenVxeGrid, { reload }] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
querySchema,
|
||||
queryList: async (params) => {
|
||||
const { data } = await getWechatMaterialList(params);
|
||||
return {
|
||||
data: data.list,
|
||||
total: data.total,
|
||||
};
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
const params = {
|
||||
page: page.currentPage,
|
||||
limit: page.pageSize,
|
||||
...formValues,
|
||||
};
|
||||
return await getWechatMaterialList(params);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
},
|
||||
columnConfig: {
|
||||
minWidth: 100,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
const gridEvents = {
|
||||
// 表格事件
|
||||
const handleToolbarClick = (code: string) => {
|
||||
switch (code) {
|
||||
case 'add':
|
||||
handleAdd();
|
||||
break;
|
||||
case 'sync':
|
||||
handleSync();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function getTypeColor(type: string) {
|
||||
const colorMap: Record<string, string> = {
|
||||
image: 'blue',
|
||||
voice: 'green',
|
||||
video: 'orange',
|
||||
news: 'purple',
|
||||
};
|
||||
return colorMap[type] || 'default';
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}分${remainingSeconds}秒`;
|
||||
}
|
||||
|
||||
function handleImageError(event: Event) {
|
||||
const target = event.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
target.nextElementSibling?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
uploadModalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleView(row: MaterialItem) {
|
||||
currentData.value = row;
|
||||
viewModalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(row: MaterialItem) {
|
||||
currentData.value = row;
|
||||
editModalVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleDelete(row: MaterialItem) {
|
||||
try {
|
||||
await deleteWechatMaterial(row.id);
|
||||
VbenMessage.success('删除成功');
|
||||
reloadTable();
|
||||
} catch (error) {
|
||||
VbenMessage.error('删除失败');
|
||||
const handleCellOperationClick = (code: string, row: WechatMaterial) => {
|
||||
switch (code) {
|
||||
case 'edit':
|
||||
currentMaterial.value = row;
|
||||
drawerVisible.value = true;
|
||||
break;
|
||||
case 'delete':
|
||||
handleDelete(row);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function handleSync() {
|
||||
const handleAdd = () => {
|
||||
currentMaterial.value = null;
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
message.loading('正在同步微信素材...');
|
||||
await syncWechatMaterial();
|
||||
VbenMessage.success('素材同步成功');
|
||||
reloadTable();
|
||||
message.success('微信素材同步成功');
|
||||
reload();
|
||||
} catch (error) {
|
||||
VbenMessage.error('素材同步失败');
|
||||
message.error('微信素材同步失败');
|
||||
console.error('同步素材失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function reloadTable() {
|
||||
gridRef.value?.reload();
|
||||
}
|
||||
const handleDelete = async (material: WechatMaterial) => {
|
||||
try {
|
||||
await message.confirm('确定要删除该素材吗?', '删除确认');
|
||||
|
||||
message.loading('正在删除素材...');
|
||||
await deleteWechatMaterial(material.id);
|
||||
message.success('素材删除成功');
|
||||
reload();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
message.error('素材删除失败');
|
||||
console.error('删除素材失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化
|
||||
});
|
||||
const handleRefresh = () => {
|
||||
reload();
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div>
|
||||
<VbenForm
|
||||
:handle-submit="handleSubmit"
|
||||
:model="model"
|
||||
:schema="formSchemas"
|
||||
:show-default-actions="false"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<template #form-submit>
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<VbenButton @click="handleCancel" variant="outline">
|
||||
{{ $t('common.cancel') }}
|
||||
</VbenButton>
|
||||
<VbenButton type="primary" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MaterialForm } from '../data';
|
||||
|
||||
import { VbenButton, VbenForm, useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { useMaterialFormSchemas } from './formSchemas';
|
||||
|
||||
interface Props {
|
||||
id?: number;
|
||||
materialData?: any;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', data: MaterialForm): void;
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
id: undefined,
|
||||
materialData: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const [Drawer] = useVbenDrawer();
|
||||
const model = ref<MaterialForm>({
|
||||
type: 'image',
|
||||
show_cover_pic: 0,
|
||||
});
|
||||
|
||||
const formSchemas = useMaterialFormSchemas();
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await Drawer?.formApi.validate();
|
||||
const formValues = Drawer?.formApi.getValues() || model.value;
|
||||
emit('submit', formValues);
|
||||
} catch (error) {
|
||||
console.error('Form validation failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
// Load material data if editing
|
||||
onMounted(async () => {
|
||||
if (props.id && props.materialData) {
|
||||
model.value = { ...props.materialData };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,156 @@
|
||||
import type { MaterialForm } from '../data';
|
||||
|
||||
import { useVbenForm } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { typeOptions, showCoverOptions } from '../data';
|
||||
|
||||
export const useMaterialFormSchemas = () => {
|
||||
const formSchemas = computed(() => [
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'type',
|
||||
label: '素材类型',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: typeOptions,
|
||||
placeholder: '请选择素材类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'title',
|
||||
label: '标题',
|
||||
rules: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return ['video', 'news'].includes(form.type) ? 'required' : '';
|
||||
}),
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return ['video', 'news'].includes(form.type);
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'introduction',
|
||||
label: '简介',
|
||||
componentProps: {
|
||||
placeholder: '请输入简介',
|
||||
maxlength: 200,
|
||||
showCount: true,
|
||||
rows: 3,
|
||||
},
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'video';
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'Upload',
|
||||
fieldName: 'url',
|
||||
label: '素材文件',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
accept: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
switch (form.type) {
|
||||
case 'image':
|
||||
return 'image/*';
|
||||
case 'voice':
|
||||
return 'audio/*';
|
||||
case 'video':
|
||||
return 'video/*';
|
||||
case 'thumb':
|
||||
return 'image/*';
|
||||
default:
|
||||
return '*';
|
||||
}
|
||||
}),
|
||||
maxCount: 1,
|
||||
showUploadList: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Upload',
|
||||
fieldName: 'thumb_url',
|
||||
label: '缩略图',
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'video';
|
||||
}),
|
||||
componentProps: {
|
||||
accept: 'image/*',
|
||||
maxCount: 1,
|
||||
showUploadList: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'content',
|
||||
label: '图文内容',
|
||||
rules: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'news' ? 'required' : '';
|
||||
}),
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'news';
|
||||
}),
|
||||
componentProps: {
|
||||
placeholder: '请输入图文内容',
|
||||
rows: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'digest',
|
||||
label: '图文摘要',
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'news';
|
||||
}),
|
||||
componentProps: {
|
||||
placeholder: '请输入图文摘要',
|
||||
maxlength: 120,
|
||||
showCount: true,
|
||||
rows: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'show_cover_pic',
|
||||
label: '封面显示',
|
||||
defaultValue: 0,
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'news';
|
||||
}),
|
||||
componentProps: {
|
||||
options: showCoverOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'author',
|
||||
label: '作者',
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'news';
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'content_source_url',
|
||||
label: '原文链接',
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'news';
|
||||
}),
|
||||
componentProps: {
|
||||
placeholder: '请输入原文链接',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return formSchemas;
|
||||
};
|
||||
@@ -0,0 +1,427 @@
|
||||
<template>
|
||||
<VbenDrawer
|
||||
v-model:show="isShow"
|
||||
:title="drawerTitle"
|
||||
:loading="loading"
|
||||
width="700px"
|
||||
@confirm="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<VbenForm
|
||||
v-model:model="formModel"
|
||||
v-model:schema="formSchema"
|
||||
:label-width="100"
|
||||
@submit="handleConfirm"
|
||||
>
|
||||
<template #fileUpload="{ model, field }">
|
||||
<div class="upload-container">
|
||||
<a-upload
|
||||
v-if="showFileUpload"
|
||||
:file-list="fileList"
|
||||
:before-upload="beforeUpload"
|
||||
:accept="acceptFileTypes"
|
||||
:multiple="false"
|
||||
@change="handleFileChange"
|
||||
>
|
||||
<a-button>
|
||||
<UploadOutlined />
|
||||
选择文件
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<div v-if="uploadedFile" class="file-info">
|
||||
<div class="file-preview">
|
||||
<img
|
||||
v-if="isImageType && uploadedFile.url"
|
||||
:src="uploadedFile.url"
|
||||
class="preview-image"
|
||||
alt="预览"
|
||||
/>
|
||||
<div v-else class="file-icon">
|
||||
<FileTextOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-details">
|
||||
<div class="file-name">{{ uploadedFile.name }}</div>
|
||||
<div class="file-size">{{ formatFileSize(uploadedFile.size) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { useVbenForm, useVbenDrawer } from '@vben/common-ui';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { UploadOutlined, FileTextOutlined } from '@ant-design/icons-vue';
|
||||
import { uploadWechatMaterial, updateWechatMaterial } from '#/api/core/wechat';
|
||||
import type { WechatMaterial, MaterialForm } from '../data';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
material?: WechatMaterial | null;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'success'): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const loading = ref(false);
|
||||
const fileList = ref<any[]>([]);
|
||||
const uploadedFile = ref<any>(null);
|
||||
|
||||
const showFileUpload = computed(() => {
|
||||
return !props.material || formModel.value.type !== props.material.type;
|
||||
});
|
||||
|
||||
const isImageType = computed(() => {
|
||||
return formModel.value.type === 'image' || formModel.value.type === 'thumb';
|
||||
});
|
||||
|
||||
const acceptFileTypes = computed(() => {
|
||||
switch (formModel.value.type) {
|
||||
case 'image':
|
||||
case 'thumb':
|
||||
return 'image/*';
|
||||
case 'voice':
|
||||
return 'audio/*';
|
||||
case 'video':
|
||||
return 'video/*';
|
||||
default:
|
||||
return '*';
|
||||
}
|
||||
});
|
||||
|
||||
const drawerTitle = computed(() => {
|
||||
return props.material ? '编辑素材' : '上传素材';
|
||||
});
|
||||
|
||||
const [VbenForm, formModel, formSchema] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '素材类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择素材类型',
|
||||
options: [
|
||||
{ label: '图片', value: 'image' },
|
||||
{ label: '语音', value: 'voice' },
|
||||
{ label: '视频', value: 'video' },
|
||||
{ label: '图文', value: 'news' },
|
||||
{ label: '缩略图', value: 'thumb' },
|
||||
],
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'file',
|
||||
label: '文件上传',
|
||||
component: 'Slot',
|
||||
slot: 'fileUpload',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
if: ({ type }) => type !== 'news',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'title',
|
||||
label: '标题',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入标题',
|
||||
maxlength: 100,
|
||||
showCount: true,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'introduction',
|
||||
label: '简介',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
placeholder: '请输入简介',
|
||||
rows: 3,
|
||||
maxlength: 200,
|
||||
showCount: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
if: ({ type }) => type === 'video',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'content',
|
||||
label: '内容',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
placeholder: '请输入图文内容',
|
||||
rows: 8,
|
||||
maxlength: 2000,
|
||||
showCount: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
if: ({ type }) => type === 'news',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'digest',
|
||||
label: '摘要',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
placeholder: '请输入摘要',
|
||||
rows: 2,
|
||||
maxlength: 120,
|
||||
showCount: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
if: ({ type }) => type === 'news',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'author',
|
||||
label: '作者',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入作者',
|
||||
maxlength: 50,
|
||||
showCount: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
if: ({ type }) => type === 'news',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'content_source_url',
|
||||
label: '原文链接',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入原文链接',
|
||||
maxlength: 200,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
if: ({ type }) => type === 'news',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'show_cover_pic',
|
||||
label: '显示封面',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '不显示', value: 0 },
|
||||
{ label: '显示', value: 1 },
|
||||
],
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
if: ({ type }) => type === 'news',
|
||||
},
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-1',
|
||||
});
|
||||
|
||||
const [VbenDrawer, isShow] = useVbenDrawer({
|
||||
formModel,
|
||||
formSchema,
|
||||
});
|
||||
|
||||
// 监听props变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
isShow.value = val;
|
||||
if (val) {
|
||||
if (props.material) {
|
||||
// 编辑模式
|
||||
formModel.value = {
|
||||
id: props.material.id,
|
||||
type: props.material.type,
|
||||
title: props.material.title || '',
|
||||
introduction: props.material.introduction || '',
|
||||
content: props.material.content || '',
|
||||
digest: props.material.digest || '',
|
||||
author: props.material.author || '',
|
||||
content_source_url: props.material.content_source_url || '',
|
||||
show_cover_pic: props.material.show_cover_pic || 0,
|
||||
};
|
||||
uploadedFile.value = {
|
||||
url: props.material.url,
|
||||
name: props.material.title || '已上传文件',
|
||||
};
|
||||
} else {
|
||||
// 新增模式
|
||||
formModel.value = {
|
||||
type: 'image',
|
||||
title: '',
|
||||
introduction: '',
|
||||
content: '',
|
||||
digest: '',
|
||||
author: '',
|
||||
content_source_url: '',
|
||||
show_cover_pic: 0,
|
||||
};
|
||||
uploadedFile.value = null;
|
||||
fileList.value = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(isShow, (val) => {
|
||||
emit('update:modelValue', val);
|
||||
});
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
if (!isLt2M) {
|
||||
message.error('文件大小不能超过 2MB!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleFileChange = (info: any) => {
|
||||
const file = info.file;
|
||||
if (file.status === 'done' || file.originFileObj) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
uploadedFile.value = {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
url: e.target?.result as string,
|
||||
file: file.originFileObj || file,
|
||||
};
|
||||
};
|
||||
if (isImageType.value) {
|
||||
reader.readAsDataURL(file.originFileObj || file);
|
||||
} else {
|
||||
uploadedFile.value = {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
file: file.originFileObj || file,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
// 构建提交数据
|
||||
const data: MaterialForm = {
|
||||
...formModel.value,
|
||||
};
|
||||
|
||||
// 如果有文件需要上传
|
||||
if (uploadedFile.value?.file && formModel.value.type !== 'news') {
|
||||
const formData = new FormData();
|
||||
formData.append('file', uploadedFile.value.file);
|
||||
formData.append('type', formModel.value.type);
|
||||
formData.append('title', formModel.value.title);
|
||||
if (formModel.value.introduction) {
|
||||
formData.append('introduction', formModel.value.introduction);
|
||||
}
|
||||
|
||||
await uploadWechatMaterial(formData);
|
||||
message.success('素材上传成功');
|
||||
} else if (props.material) {
|
||||
// 编辑模式
|
||||
await updateWechatMaterial(data);
|
||||
message.success('素材更新成功');
|
||||
} else if (formModel.value.type === 'news') {
|
||||
// 图文消息新增
|
||||
await uploadWechatMaterial(data);
|
||||
message.success('图文消息创建成功');
|
||||
}
|
||||
|
||||
emit('success');
|
||||
isShow.value = false;
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
console.error('操作失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
isShow.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #e6f7ff;
|
||||
border-radius: 4px;
|
||||
font-size: 32px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-medium mb-2">素材详情</h3>
|
||||
<div class="text-sm text-gray-600">
|
||||
<p><strong>MediaID:</strong> {{ materialData.media_id }}</p>
|
||||
<p><strong>类型:</strong> {{ getTypeLabel(materialData.type) }}</p>
|
||||
<p><strong>创建时间:</strong> {{ materialData.create_time }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium mb-2">基本信息</h4>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div v-if="materialData.title" class="mb-2">
|
||||
<strong>标题:</strong> {{ materialData.title }}
|
||||
</div>
|
||||
<div v-if="materialData.author" class="mb-2">
|
||||
<strong>作者:</strong> {{ materialData.author }}
|
||||
</div>
|
||||
<div v-if="materialData.digest" class="mb-2">
|
||||
<strong>摘要:</strong> {{ materialData.digest }}
|
||||
</div>
|
||||
<div v-if="materialData.introduction" class="mb-2">
|
||||
<strong>简介:</strong> {{ materialData.introduction }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="materialData.type === 'image'" class="mb-4">
|
||||
<h4 class="font-medium mb-2">图片预览</h4>
|
||||
<div class="text-center">
|
||||
<img
|
||||
:src="materialData.url"
|
||||
alt="图片素材"
|
||||
class="max-w-md max-h-96 object-contain border rounded-lg mx-auto"
|
||||
@error="(e: any) => e.target.src = 'https://via.placeholder.com/400x300'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="materialData.type === 'video'" class="mb-4">
|
||||
<h4 class="font-medium mb-2">视频预览</h4>
|
||||
<div class="text-center">
|
||||
<video
|
||||
:src="materialData.url"
|
||||
controls
|
||||
class="max-w-md max-h-96 border rounded-lg mx-auto"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
<div v-if="materialData.thumb_url" class="mt-2 text-center">
|
||||
<p class="text-sm text-gray-600 mb-1">缩略图</p>
|
||||
<img
|
||||
:src="materialData.thumb_url"
|
||||
alt="缩略图"
|
||||
class="w-20 h-20 object-cover border rounded"
|
||||
@error="(e: any) => e.target.src = 'https://via.placeholder.com/80x80'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="materialData.type === 'voice'" class="mb-4">
|
||||
<h4 class="font-medium mb-2">音频预览</h4>
|
||||
<div class="text-center">
|
||||
<audio
|
||||
:src="materialData.url"
|
||||
controls
|
||||
class="max-w-md mx-auto"
|
||||
>
|
||||
您的浏览器不支持音频播放
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="materialData.type === 'news'" class="mb-4">
|
||||
<h4 class="font-medium mb-2">图文内容</h4>
|
||||
<div class="bg-white border rounded-lg p-4">
|
||||
<div v-if="materialData.show_cover_pic === 1 && materialData.url" class="mb-4">
|
||||
<img
|
||||
:src="materialData.url"
|
||||
alt="封面图"
|
||||
class="w-full h-48 object-cover rounded"
|
||||
@error="(e: any) => e.target.src = 'https://via.placeholder.com/400x200'"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="materialData.content"
|
||||
class="prose max-w-none"
|
||||
v-html="materialData.content"
|
||||
></div>
|
||||
<div v-if="materialData.content_source_url" class="mt-4">
|
||||
<a
|
||||
:href="materialData.content_source_url"
|
||||
target="_blank"
|
||||
class="text-blue-600 hover:text-blue-800 underline"
|
||||
>
|
||||
阅读原文
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-2">
|
||||
<VbenButton @click="handleClose" variant="outline">
|
||||
{{ $t('common.close') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { VbenButton } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
interface Props {
|
||||
materialData: any;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
function getTypeLabel(type: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
'image': '图片',
|
||||
'voice': '语音',
|
||||
'video': '视频',
|
||||
'news': '图文',
|
||||
'thumb': '缩略图',
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('cancel');
|
||||
}
|
||||
</script>
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { VxeGridProps } from '@vben/plugins/vxe-table';
|
||||
|
||||
export interface MenuItem {
|
||||
id: number;
|
||||
@@ -6,35 +6,35 @@ export interface MenuItem {
|
||||
type: 'click' | 'view' | 'miniprogram' | 'scancode_push' | 'scancode_waitmsg' | 'pic_sysphoto' | 'pic_photo_or_album' | 'pic_weixin' | 'location_select';
|
||||
key?: string;
|
||||
url?: string;
|
||||
media_id?: string;
|
||||
appid?: string;
|
||||
pagepath?: string;
|
||||
media_id?: string;
|
||||
parent_id: number;
|
||||
sort: number;
|
||||
status: 0 | 1;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
export interface MenuForm {
|
||||
id?: number;
|
||||
name: string;
|
||||
type: 'click' | 'view' | 'miniprogram' | 'scancode_push' | 'scancode_waitmsg' | 'pic_sysphoto' | 'pic_photo_or_album' | 'pic_weixin' | 'location_select';
|
||||
type: string;
|
||||
key?: string;
|
||||
url?: string;
|
||||
media_id?: string;
|
||||
appid?: string;
|
||||
pagepath?: string;
|
||||
media_id?: string;
|
||||
parent_id: number;
|
||||
sort: number;
|
||||
status: 0 | 1;
|
||||
}
|
||||
|
||||
export const menuTypeOptions = [
|
||||
export const typeOptions = [
|
||||
{ label: '点击推事件', value: 'click' },
|
||||
{ label: '跳转URL', value: 'view' },
|
||||
{ label: '扫码推事件', value: 'scancode_push' },
|
||||
{ label: '扫码推事件且弹出提示', value: 'scancode_waitmsg' },
|
||||
{ label: '扫码推事件且弹出消息接收中', value: 'scancode_waitmsg' },
|
||||
{ label: '弹出系统拍照发图', value: 'pic_sysphoto' },
|
||||
{ label: '弹出拍照或者相册发图', value: 'pic_photo_or_album' },
|
||||
{ label: '弹出微信相册发图器', value: 'pic_weixin' },
|
||||
@@ -42,70 +42,60 @@ export const menuTypeOptions = [
|
||||
{ label: '跳转小程序', value: 'miniprogram' },
|
||||
];
|
||||
|
||||
export const menuTypeMap = {
|
||||
click: '点击推事件',
|
||||
view: '跳转URL',
|
||||
scancode_push: '扫码推事件',
|
||||
scancode_waitmsg: '扫码推事件且弹出提示',
|
||||
pic_sysphoto: '弹出系统拍照发图',
|
||||
pic_photo_or_album: '弹出拍照或者相册发图',
|
||||
pic_weixin: '弹出微信相册发图器',
|
||||
location_select: '弹出地理位置选择器',
|
||||
miniprogram: '跳转小程序',
|
||||
};
|
||||
|
||||
export const statusOptions = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
];
|
||||
|
||||
export const statusMap = {
|
||||
1: '启用',
|
||||
0: '禁用',
|
||||
};
|
||||
|
||||
export const querySchema = [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '菜单名称',
|
||||
component: 'Input',
|
||||
export const gridOptions: VxeGridProps<MenuItem> = {
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'name', title: '菜单名称', minWidth: 150, treeNode: true },
|
||||
{ field: 'type', title: '菜单类型', width: 120, formatter: ({ cellValue }) => {
|
||||
const option = typeOptions.find(item => item.value === cellValue);
|
||||
return option?.label || cellValue;
|
||||
}},
|
||||
{ field: 'key', title: '菜单KEY', width: 150 },
|
||||
{ field: 'url', title: '跳转URL', minWidth: 200 },
|
||||
{ field: 'sort', title: '排序', width: 80 },
|
||||
{ field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '启用' : '禁用';
|
||||
}},
|
||||
{ field: 'create_time', title: '创建时间', width: 180 },
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: MenuItem) => {
|
||||
// This will be handled in the component
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '菜单类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: menuTypeOptions,
|
||||
placeholder: '请选择菜单类型',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// This will be implemented in the component
|
||||
return { rows: [], total: 0 };
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: statusOptions,
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
print: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ field: 'name', title: '菜单名称', minWidth: 150 },
|
||||
{ field: 'type', title: '菜单类型', width: 150, slots: { default: 'menuType' } },
|
||||
{ field: 'key', title: '菜单KEY', width: 150 },
|
||||
{ field: 'url', title: '跳转URL', minWidth: 200, showOverflow: true },
|
||||
{ field: 'sort', title: '排序', width: 80 },
|
||||
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } },
|
||||
{ field: 'create_time', title: '创建时间', width: 180 },
|
||||
{ field: 'update_time', title: '更新时间', width: 180 },
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,176 +1,156 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="h-full">
|
||||
<VbenVxeGrid
|
||||
ref="gridRef"
|
||||
:form-options="formOptions"
|
||||
:grid-options="gridOptions"
|
||||
:grid-events="gridEvents"
|
||||
:query-schema="querySchema"
|
||||
title="自定义菜单管理"
|
||||
@toolbar-button-click="handleToolbarClick"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<VbenButton type="primary" @click="handleAdd">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
新增菜单
|
||||
</VbenButton>
|
||||
<VbenButton type="success" @click="handlePublish">
|
||||
<Upload class="mr-2 h-4 w-4" />
|
||||
<Button type="primary" @click="handleSync">
|
||||
<Icon icon="ant-design:sync-outlined" />
|
||||
同步菜单
|
||||
</Button>
|
||||
<Button type="primary" @click="handlePublish">
|
||||
<Icon icon="ant-design:cloud-upload-outlined" />
|
||||
发布菜单
|
||||
</VbenButton>
|
||||
</template>
|
||||
|
||||
<template #menuType="{ row }">
|
||||
<VbenTag :type="getMenuTypeColor(row.type)">
|
||||
{{ menuTypeMap[row.type] }}
|
||||
</VbenTag>
|
||||
</template>
|
||||
|
||||
<template #status="{ row }">
|
||||
<VbenTag :type="row.status === 1 ? 'success' : 'error'">
|
||||
{{ statusMap[row.status] }}
|
||||
</VbenTag>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<VbenButton
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</VbenButton>
|
||||
<VbenPopconfirm
|
||||
title="确定删除该菜单吗?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<VbenButton type="text" size="small" danger>
|
||||
删除
|
||||
</VbenButton>
|
||||
</VbenPopconfirm>
|
||||
</Button>
|
||||
</template>
|
||||
</VbenVxeGrid>
|
||||
|
||||
<MenuEditModal
|
||||
v-model="modalVisible"
|
||||
:data="currentData"
|
||||
:parent-menus="parentMenus"
|
||||
@reload="reloadTable"
|
||||
/>
|
||||
<VbenDrawer
|
||||
v-model:show="drawerShow"
|
||||
:title="drawerTitle"
|
||||
:width="800"
|
||||
>
|
||||
<MenuForm
|
||||
:id="currentId"
|
||||
@success="handleSuccess"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</VbenDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { Plus, Upload } from '@vben/icons';
|
||||
import { ref } from 'vue';
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { useVbenVxeGrid, VbenDrawer } from '#/adapter';
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { VbenButton, VbenMessage, VbenPopconfirm, VbenTag } from '@vben/common-ui';
|
||||
import { gridOptions } from './data';
|
||||
import MenuForm from './modules/menu-form.vue';
|
||||
import { deleteWechatMenu, syncWechatMenu, publishWechatMenu } from '#/api';
|
||||
|
||||
import { getWechatMenuList, deleteWechatMenu, publishWechatMenu } from '#/api/core/wechat';
|
||||
import MenuEditModal from './modules/menu-edit.vue';
|
||||
const drawerShow = ref(false);
|
||||
const drawerTitle = ref('');
|
||||
const currentId = ref<number | null>(null);
|
||||
|
||||
import type { MenuItem } from './data';
|
||||
import { columns, querySchema, menuTypeMap, statusMap } from './data';
|
||||
|
||||
const modalVisible = ref(false);
|
||||
const currentData = ref<MenuItem | null>(null);
|
||||
const menuList = ref<MenuItem[]>([]);
|
||||
|
||||
const gridRef = ref();
|
||||
|
||||
const formOptions = computed(() => ({
|
||||
schema: querySchema,
|
||||
showCollapseButton: false,
|
||||
fieldSize: 'medium',
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-3 lg:grid-cols-4',
|
||||
}));
|
||||
|
||||
const gridOptions = computed(() => ({
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
const params = {
|
||||
page: page.currentPage,
|
||||
limit: page.pageSize,
|
||||
...formValues,
|
||||
};
|
||||
const response = await getWechatMenuList(params);
|
||||
menuList.value = response.data;
|
||||
return response;
|
||||
},
|
||||
const querySchema = [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '菜单名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入菜单名称',
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '菜单类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择菜单类型',
|
||||
options: [
|
||||
{ label: '点击推事件', value: 'click' },
|
||||
{ label: '跳转URL', value: 'view' },
|
||||
{ label: '扫码推事件', value: 'scancode_push' },
|
||||
{ label: '扫码推事件且弹出消息接收中', value: 'scancode_waitmsg' },
|
||||
{ label: '弹出系统拍照发图', value: 'pic_sysphoto' },
|
||||
{ label: '弹出拍照或者相册发图', value: 'pic_photo_or_album' },
|
||||
{ label: '弹出微信相册发图器', value: 'pic_weixin' },
|
||||
{ label: '弹出地理位置选择器', value: 'location_select' },
|
||||
{ label: '跳转小程序', value: 'miniprogram' },
|
||||
],
|
||||
},
|
||||
},
|
||||
columnConfig: {
|
||||
minWidth: 100,
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
}));
|
||||
];
|
||||
|
||||
const gridEvents = {
|
||||
// 表格事件
|
||||
};
|
||||
|
||||
const parentMenus = computed(() => {
|
||||
return menuList.value.filter(item => item.parent_id === 0);
|
||||
});
|
||||
|
||||
function getMenuTypeColor(type: string) {
|
||||
const colorMap: Record<string, string> = {
|
||||
click: 'blue',
|
||||
view: 'green',
|
||||
miniprogram: 'orange',
|
||||
scancode_push: 'purple',
|
||||
scancode_waitmsg: 'purple',
|
||||
pic_sysphoto: 'pink',
|
||||
pic_photo_or_album: 'pink',
|
||||
pic_weixin: 'pink',
|
||||
location_select: 'cyan',
|
||||
};
|
||||
return colorMap[type] || 'default';
|
||||
function handleToolbarClick(code: string, row: any) {
|
||||
switch (code) {
|
||||
case 'add':
|
||||
handleAdd();
|
||||
break;
|
||||
case 'edit':
|
||||
handleEdit(row);
|
||||
break;
|
||||
case 'delete':
|
||||
handleDelete(row);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
currentData.value = null;
|
||||
modalVisible.value = true;
|
||||
drawerTitle.value = '新增菜单';
|
||||
currentId.value = null;
|
||||
drawerShow.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(row: MenuItem) {
|
||||
currentData.value = row;
|
||||
modalVisible.value = true;
|
||||
function handleEdit(row: any) {
|
||||
drawerTitle.value = '编辑菜单';
|
||||
currentId.value = row.id;
|
||||
drawerShow.value = true;
|
||||
}
|
||||
|
||||
async function handleDelete(row: MenuItem) {
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await deleteWechatMenu(row.id);
|
||||
VbenMessage.success('删除成功');
|
||||
reloadTable();
|
||||
// Refresh grid
|
||||
} catch (error) {
|
||||
VbenMessage.error('删除失败');
|
||||
console.error('删除菜单失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSync() {
|
||||
try {
|
||||
await syncWechatMenu();
|
||||
// Refresh grid
|
||||
} catch (error) {
|
||||
console.error('同步菜单失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePublish() {
|
||||
try {
|
||||
await publishWechatMenu();
|
||||
VbenMessage.success('菜单发布成功');
|
||||
// Refresh grid
|
||||
} catch (error) {
|
||||
VbenMessage.error('菜单发布失败');
|
||||
console.error('发布菜单失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
gridRef.value?.reload();
|
||||
function handleSuccess() {
|
||||
drawerShow.value = false;
|
||||
// Refresh grid
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化
|
||||
});
|
||||
function handleCancel() {
|
||||
drawerShow.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div>
|
||||
<VbenForm
|
||||
:handle-submit="handleSubmit"
|
||||
:model="model"
|
||||
:schema="formSchemas"
|
||||
:show-default-actions="false"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<template #form-submit>
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<VbenButton @click="handleCancel" variant="outline">
|
||||
{{ $t('common.cancel') }}
|
||||
</VbenButton>
|
||||
<VbenButton type="primary" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MenuForm } from '../data';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { VbenButton, VbenForm, useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { useMenuFormSchemas } from './formSchemas';
|
||||
|
||||
interface Props {
|
||||
id?: number;
|
||||
menuTree: any[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', data: MenuForm): void;
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
id: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const [Drawer] = useVbenDrawer();
|
||||
const model = ref<MenuForm>({
|
||||
name: '',
|
||||
type: 'click',
|
||||
parent_id: 0,
|
||||
sort: 0,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const formSchemas = useMenuFormSchemas();
|
||||
|
||||
const treeData = computed(() => {
|
||||
const tree = props.menuTree.map(item => ({
|
||||
title: item.name,
|
||||
value: item.id,
|
||||
key: item.id,
|
||||
children: item.children?.map(child => ({
|
||||
title: child.name,
|
||||
value: child.id,
|
||||
key: child.id,
|
||||
})) || [],
|
||||
}));
|
||||
|
||||
return [
|
||||
{ title: '顶级菜单', value: 0, key: 0 },
|
||||
...tree,
|
||||
];
|
||||
});
|
||||
|
||||
// Update form schemas with dynamic tree data
|
||||
watchEffect(() => {
|
||||
const schemas = formSchemas.value;
|
||||
const parentSchema = schemas.find(schema => schema.fieldName === 'parent_id');
|
||||
if (parentSchema && parentSchema.componentProps) {
|
||||
parentSchema.componentProps.treeData = treeData.value;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await Drawer?.formApi.validate();
|
||||
const formValues = Drawer?.formApi.getValues() || model.value;
|
||||
emit('submit', formValues);
|
||||
} catch (error) {
|
||||
console.error('Form validation failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
// Load menu data if editing
|
||||
onMounted(async () => {
|
||||
if (props.id) {
|
||||
try {
|
||||
// Load menu data
|
||||
const menuData = await getWechatMenuDetailApi(props.id);
|
||||
model.value = { ...menuData };
|
||||
} catch (error) {
|
||||
console.error('Failed to load menu data:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { MenuForm, MenuItem } from './data';
|
||||
|
||||
import { useVbenForm } from '@vben/common-ui';
|
||||
import { getI18nOptions } from '@vben/locale';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { typeOptions, statusOptions } from './data';
|
||||
|
||||
export const useMenuFormSchemas = () => {
|
||||
const formSchemas = computed(() => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '菜单名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'type',
|
||||
label: '菜单类型',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: typeOptions,
|
||||
placeholder: '请选择菜单类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'key',
|
||||
label: '菜单KEY',
|
||||
rules: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'click' ? 'required' : '';
|
||||
}),
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return ['click', 'scancode_push', 'scancode_waitmsg', 'pic_sysphoto', 'pic_photo_or_album', 'pic_weixin', 'location_select'].includes(form.type);
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'url',
|
||||
label: '跳转URL',
|
||||
rules: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'view' ? 'required|url' : '';
|
||||
}),
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'view';
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'appid',
|
||||
label: '小程序AppID',
|
||||
rules: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'miniprogram' ? 'required' : '';
|
||||
}),
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'miniprogram';
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'pagepath',
|
||||
label: '小程序页面路径',
|
||||
rules: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'miniprogram' ? 'required' : '';
|
||||
}),
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'miniprogram';
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'TreeSelect',
|
||||
fieldName: 'parent_id',
|
||||
label: '上级菜单',
|
||||
componentProps: {
|
||||
placeholder: '请选择上级菜单',
|
||||
treeDefaultExpandAll: false,
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'sort',
|
||||
label: '排序',
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
max: 999,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
defaultValue: 1,
|
||||
componentProps: {
|
||||
options: statusOptions,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return formSchemas;
|
||||
};
|
||||
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<VbenForm
|
||||
:schema="formSchema"
|
||||
:handle-submit="handleSubmit"
|
||||
:submit-button-options="{ text: '保存' }"
|
||||
:reset-button-options="{ show: false }"
|
||||
wrapper-class="!grid-cols-1 md:!grid-cols-2"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { $t } from '#/locales';
|
||||
import { getWechatMenuInfo, createWechatMenu, updateWechatMenu } from '#/api';
|
||||
|
||||
interface Props {
|
||||
id?: number | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
id: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const menuInfo = ref<any>(null);
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '点击推事件', value: 'click' },
|
||||
{ label: '跳转URL', value: 'view' },
|
||||
{ label: '扫码推事件', value: 'scancode_push' },
|
||||
{ label: '扫码推事件且弹出消息接收中', value: 'scancode_waitmsg' },
|
||||
{ label: '弹出系统拍照发图', value: 'pic_sysphoto' },
|
||||
{ label: '弹出拍照或者相册发图', value: 'pic_photo_or_album' },
|
||||
{ label: '弹出微信相册发图器', value: 'pic_weixin' },
|
||||
{ label: '弹出地理位置选择器', value: 'location_select' },
|
||||
{ label: '跳转小程序', value: 'miniprogram' },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
];
|
||||
|
||||
const formSchema = computed(() => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '菜单名称',
|
||||
rules: 'required|max:16',
|
||||
componentProps: {
|
||||
placeholder: '请输入菜单名称,不超过16个字节',
|
||||
maxLength: 16,
|
||||
showCount: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'type',
|
||||
label: '菜单类型',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请选择菜单类型',
|
||||
options: typeOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'key',
|
||||
label: '菜单KEY',
|
||||
rules: 'max:128',
|
||||
componentProps: {
|
||||
placeholder: '请输入菜单KEY,不超过128字节',
|
||||
maxLength: 128,
|
||||
showCount: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
if: ({ type }) => type === 'click',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'url',
|
||||
label: '跳转URL',
|
||||
rules: 'required|url|max:1024',
|
||||
componentProps: {
|
||||
placeholder: '请输入跳转URL,不超过1024字节',
|
||||
maxLength: 1024,
|
||||
showCount: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
if: ({ type }) => type === 'view',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'media_id',
|
||||
label: '媒体ID',
|
||||
rules: 'max:64',
|
||||
componentProps: {
|
||||
placeholder: '请输入媒体ID,不超过64字节',
|
||||
maxLength: 64,
|
||||
showCount: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
if: ({ type }) => ['pic_sysphoto', 'pic_photo_or_album', 'pic_weixin'].includes(type),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'appid',
|
||||
label: '小程序APPID',
|
||||
rules: 'max:32',
|
||||
componentProps: {
|
||||
placeholder: '请输入小程序APPID,不超过32字节',
|
||||
maxLength: 32,
|
||||
showCount: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
if: ({ type }) => type === 'miniprogram',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'pagepath',
|
||||
label: '小程序页面路径',
|
||||
rules: 'max:128',
|
||||
componentProps: {
|
||||
placeholder: '请输入小程序页面路径,不超过128字节',
|
||||
maxLength: 128,
|
||||
showCount: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
if: ({ type }) => type === 'miniprogram',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'TreeSelect',
|
||||
fieldName: 'parent_id',
|
||||
label: '上级菜单',
|
||||
componentProps: {
|
||||
placeholder: '请选择上级菜单,不选则为一级菜单',
|
||||
treeData: [], // This will be loaded from API
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'sort',
|
||||
label: '排序',
|
||||
rules: 'required|integer|min:0',
|
||||
componentProps: {
|
||||
placeholder: '请输入排序',
|
||||
min: 0,
|
||||
max: 999,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
rules: 'required',
|
||||
defaultValue: 1,
|
||||
componentProps: {
|
||||
options: statusOptions,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
async function loadMenuInfo() {
|
||||
if (!props.id) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await getWechatMenuInfo(props.id);
|
||||
menuInfo.value = res.data;
|
||||
} catch (error) {
|
||||
message.error('获取菜单信息失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(values: any) {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const data = {
|
||||
...values,
|
||||
id: props.id,
|
||||
};
|
||||
|
||||
if (props.id) {
|
||||
await updateWechatMenu(data);
|
||||
message.success('更新菜单成功');
|
||||
} else {
|
||||
await createWechatMenu(data);
|
||||
message.success('创建菜单成功');
|
||||
}
|
||||
|
||||
emit('success');
|
||||
} catch (error) {
|
||||
message.error(props.id ? '更新菜单失败' : '创建菜单失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.id, loadMenuInfo, { immediate: true });
|
||||
</script>
|
||||
@@ -1,33 +1,32 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { VxeGridProps } from '@vben/plugins/vxe-table';
|
||||
|
||||
export interface UserItem {
|
||||
export interface WechatUser {
|
||||
id: number;
|
||||
openid: string;
|
||||
nickname: string;
|
||||
headimgurl: string;
|
||||
sex: 0 | 1 | 2; // 0:未知, 1:男, 2:女
|
||||
language: string;
|
||||
city: string;
|
||||
province: string;
|
||||
country: string;
|
||||
headimgurl: string;
|
||||
subscribe: 0 | 1; // 0:未关注, 1:已关注
|
||||
subscribe_time: string;
|
||||
unsubscribe_time?: string;
|
||||
unionid?: string;
|
||||
remark: string;
|
||||
groupid: number;
|
||||
tagid_list: string;
|
||||
subscribe_scene: string;
|
||||
qr_scene?: string;
|
||||
qr_scene_str?: string;
|
||||
remark: string;
|
||||
language: string;
|
||||
qr_scene: string;
|
||||
qr_scene_str: string;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
export interface UserForm {
|
||||
export interface WechatUserForm {
|
||||
id?: number;
|
||||
openid: string;
|
||||
remark?: string;
|
||||
remark: string;
|
||||
groupid?: number;
|
||||
}
|
||||
|
||||
@@ -37,109 +36,60 @@ export const sexOptions = [
|
||||
{ label: '女', value: 2 },
|
||||
];
|
||||
|
||||
export const sexMap = {
|
||||
0: '未知',
|
||||
1: '男',
|
||||
2: '女',
|
||||
};
|
||||
|
||||
export const subscribeOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '已关注', value: 1 },
|
||||
{ label: '未关注', value: 0 },
|
||||
];
|
||||
|
||||
export const subscribeMap = {
|
||||
1: '已关注',
|
||||
0: '未关注',
|
||||
};
|
||||
|
||||
export const querySchema = [
|
||||
{
|
||||
fieldName: 'nickname',
|
||||
label: '昵称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'openid',
|
||||
label: 'OpenID',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: sexOptions,
|
||||
placeholder: '请选择性别',
|
||||
export const gridOptions: VxeGridProps<WechatUser> = {
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'headimgurl', title: '头像', width: 80, formatter: ({ cellValue }) => {
|
||||
return cellValue ? `<img src="${cellValue}" style="width: 40px; height: 40px; border-radius: 50%;" />` : '';
|
||||
} },
|
||||
{ field: 'nickname', title: '昵称', minWidth: 120 },
|
||||
{ field: 'sex', title: '性别', width: 80, formatter: ({ cellValue }) => {
|
||||
const option = sexOptions.find(item => item.value === cellValue);
|
||||
return option?.label || '未知';
|
||||
}},
|
||||
{ field: 'city', title: '城市', width: 100 },
|
||||
{ field: 'province', title: '省份', width: 100 },
|
||||
{ field: 'country', title: '国家', width: 100 },
|
||||
{ field: 'subscribe', title: '关注状态', width: 100, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '已关注' : '未关注';
|
||||
}},
|
||||
{ field: 'subscribe_time', title: '关注时间', width: 180 },
|
||||
{ field: 'remark', title: '备注', minWidth: 150 },
|
||||
{ field: 'groupid', title: '分组ID', width: 100 },
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: WechatUser) => {
|
||||
// This will be handled in the component
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
pageSizes: [10, 20, 50, 100, 200],
|
||||
},
|
||||
{
|
||||
fieldName: 'subscribe',
|
||||
label: '关注状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: subscribeOptions,
|
||||
placeholder: '请选择关注状态',
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
print: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'city',
|
||||
label: '城市',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'province',
|
||||
label: '省份',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'country',
|
||||
label: '国家',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'subscribe_time',
|
||||
label: '关注时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
type: 'datetimerange',
|
||||
rangeSeparator: '至',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{
|
||||
field: 'headimgurl',
|
||||
title: '头像',
|
||||
width: 80,
|
||||
slots: { default: 'avatar' },
|
||||
align: 'center',
|
||||
},
|
||||
{ field: 'nickname', title: '昵称', minWidth: 150 },
|
||||
{ field: 'sex', title: '性别', width: 80, slots: { default: 'sex' } },
|
||||
{ field: 'city', title: '城市', width: 120 },
|
||||
{ field: 'province', title: '省份', width: 120 },
|
||||
{ field: 'country', title: '国家', width: 120 },
|
||||
{ field: 'subscribe', title: '关注状态', width: 100, slots: { default: 'subscribe' } },
|
||||
{ field: 'subscribe_time', title: '关注时间', width: 180 },
|
||||
{ field: 'unsubscribe_time', title: '取消关注时间', width: 180 },
|
||||
{ field: 'remark', title: '备注', minWidth: 150, showOverflow: true },
|
||||
{ field: 'groupid', title: '分组ID', width: 100 },
|
||||
{ field: 'tagid_list', title: '标签ID', minWidth: 150, showOverflow: true },
|
||||
{ field: 'language', title: '语言', width: 100 },
|
||||
{ field: 'subscribe_scene', title: '关注场景', minWidth: 150, showOverflow: true },
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,180 +1,119 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="m-4">
|
||||
<VbenVxeGrid
|
||||
ref="gridRef"
|
||||
:form-options="formOptions"
|
||||
:grid-options="gridOptions"
|
||||
:grid-events="gridEvents"
|
||||
:query-schema="querySchema"
|
||||
title="微信用户管理"
|
||||
@toolbar-button-click="handleToolbarClick"
|
||||
@cell-operation-click="handleCellOperationClick"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<template #toolbar-buttons>
|
||||
<VbenButton type="primary" @click="handleSync">
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
<template #icon>
|
||||
<SyncOutlined />
|
||||
</template>
|
||||
同步用户
|
||||
</VbenButton>
|
||||
</template>
|
||||
|
||||
<template #avatar="{ row }">
|
||||
<VbenAvatar :src="row.headimgurl" :alt="row.nickname" size="small" />
|
||||
</template>
|
||||
|
||||
<template #sex="{ row }">
|
||||
<VbenTag :type="getSexColor(row.sex)">
|
||||
{{ sexMap[row.sex] }}
|
||||
</VbenTag>
|
||||
</template>
|
||||
|
||||
<template #subscribe="{ row }">
|
||||
<VbenTag :type="row.subscribe === 1 ? 'success' : 'error'">
|
||||
{{ subscribeMap[row.subscribe] }}
|
||||
</VbenTag>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<VbenButton
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleSendMessage(row)"
|
||||
>
|
||||
发消息
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleMoveGroup(row)"
|
||||
>
|
||||
移动分组
|
||||
<VbenButton type="default" @click="handleExport">
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出用户
|
||||
</VbenButton>
|
||||
</template>
|
||||
</VbenVxeGrid>
|
||||
|
||||
<UserEditModal
|
||||
v-model="modalVisible"
|
||||
:data="currentData"
|
||||
@reload="reloadTable"
|
||||
/>
|
||||
|
||||
<SendMessageModal
|
||||
v-model="messageModalVisible"
|
||||
:user="currentData"
|
||||
@reload="reloadTable"
|
||||
/>
|
||||
|
||||
<MoveGroupModal
|
||||
v-model="groupModalVisible"
|
||||
:user="currentData"
|
||||
@reload="reloadTable"
|
||||
<UserForm
|
||||
v-model="drawerVisible"
|
||||
:user="currentUser"
|
||||
@success="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { RefreshCw } from '@vben/icons';
|
||||
import { ref } from 'vue';
|
||||
import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
import { VbenButton } from '@vben/common-ui';
|
||||
import { SyncOutlined, ExportOutlined } from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import UserForm from './modules/user-form.vue';
|
||||
import { gridOptions, querySchema } from './data';
|
||||
import { getWechatUserList, syncWechatUser, exportWechatUser } from '#/api/core/wechat';
|
||||
import type { WechatUser } from './data';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { VbenAvatar, VbenButton, VbenMessage, VbenTag } from '@vben/common-ui';
|
||||
const drawerVisible = ref(false);
|
||||
const currentUser = ref<WechatUser | null>(null);
|
||||
|
||||
import { getWechatUserList, syncWechatUser } from '#/api/core/wechat';
|
||||
import UserEditModal from './modules/user-edit.vue';
|
||||
import SendMessageModal from './modules/send-message.vue';
|
||||
import MoveGroupModal from './modules/move-group.vue';
|
||||
|
||||
import type { UserItem } from './data';
|
||||
import { columns, querySchema, sexMap, subscribeMap } from './data';
|
||||
|
||||
const modalVisible = ref(false);
|
||||
const messageModalVisible = ref(false);
|
||||
const groupModalVisible = ref(false);
|
||||
const currentData = ref<UserItem | null>(null);
|
||||
|
||||
const gridRef = ref();
|
||||
|
||||
const formOptions = computed(() => ({
|
||||
schema: querySchema,
|
||||
showCollapseButton: true,
|
||||
fieldSize: 'medium',
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
}));
|
||||
|
||||
const gridOptions = computed(() => ({
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
const [VbenVxeGrid, { reload }] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
querySchema,
|
||||
queryList: async (params) => {
|
||||
const { data } = await getWechatUserList(params);
|
||||
return {
|
||||
data: data.list,
|
||||
total: data.total,
|
||||
};
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
const params = {
|
||||
page: page.currentPage,
|
||||
limit: page.pageSize,
|
||||
...formValues,
|
||||
};
|
||||
return await getWechatUserList(params);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
},
|
||||
columnConfig: {
|
||||
minWidth: 100,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
const gridEvents = {
|
||||
// 表格事件
|
||||
const handleToolbarClick = (code: string) => {
|
||||
switch (code) {
|
||||
case 'sync':
|
||||
handleSync();
|
||||
break;
|
||||
case 'export':
|
||||
handleExport();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function getSexColor(sex: number) {
|
||||
const colorMap: Record<number, string> = {
|
||||
0: 'default',
|
||||
1: 'blue',
|
||||
2: 'pink',
|
||||
};
|
||||
return colorMap[sex] || 'default';
|
||||
}
|
||||
|
||||
function handleEdit(row: UserItem) {
|
||||
currentData.value = row;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleSendMessage(row: UserItem) {
|
||||
currentData.value = row;
|
||||
messageModalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleMoveGroup(row: UserItem) {
|
||||
currentData.value = row;
|
||||
groupModalVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSync() {
|
||||
try {
|
||||
await syncWechatUser();
|
||||
VbenMessage.success('用户同步成功');
|
||||
reloadTable();
|
||||
} catch (error) {
|
||||
VbenMessage.error('用户同步失败');
|
||||
const handleCellOperationClick = (code: string, row: WechatUser) => {
|
||||
switch (code) {
|
||||
case 'edit':
|
||||
currentUser.value = row;
|
||||
drawerVisible.value = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function reloadTable() {
|
||||
gridRef.value?.reload();
|
||||
}
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
message.loading('正在同步微信用户...');
|
||||
await syncWechatUser();
|
||||
message.success('微信用户同步成功');
|
||||
reload();
|
||||
} catch (error) {
|
||||
message.error('微信用户同步失败');
|
||||
console.error('同步用户失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化
|
||||
});
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
message.loading('正在导出用户数据...');
|
||||
const { data } = await exportWechatUser();
|
||||
|
||||
// 创建下载链接
|
||||
const link = document.createElement('a');
|
||||
link.href = data.url;
|
||||
link.download = '微信用户数据.xlsx';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
message.success('用户数据导出成功');
|
||||
} catch (error) {
|
||||
message.error('用户数据导出失败');
|
||||
console.error('导出用户失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
reload();
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<VbenForm
|
||||
:handle-submit="handleSubmit"
|
||||
:model="model"
|
||||
:schema="formSchemas"
|
||||
:show-default-actions="false"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<template #form-submit>
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<VbenButton @click="handleCancel" variant="outline">
|
||||
{{ $t('common.cancel') }}
|
||||
</VbenButton>
|
||||
<VbenButton type="primary" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { WechatUserForm } from '../data';
|
||||
|
||||
import { VbenButton, VbenForm, useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { useUserFormSchemas } from './formSchemas';
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
userData: any;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', data: WechatUserForm): void;
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const [Drawer] = useVbenDrawer();
|
||||
const model = ref<WechatUserForm>({
|
||||
remark: '',
|
||||
groupid: 0,
|
||||
});
|
||||
|
||||
const formSchemas = useUserFormSchemas();
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await Drawer?.formApi.validate();
|
||||
const formValues = Drawer?.formApi.getValues() || model.value;
|
||||
emit('submit', formValues);
|
||||
} catch (error) {
|
||||
console.error('Form validation failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
// Load user data
|
||||
onMounted(async () => {
|
||||
if (props.userData) {
|
||||
model.value = {
|
||||
remark: props.userData.remark || '',
|
||||
groupid: props.userData.groupid || 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { WechatUserForm } from '../data';
|
||||
|
||||
import { useVbenForm } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
export const useUserFormSchemas = () => {
|
||||
const formSchemas = computed(() => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
maxlength: 100,
|
||||
showCount: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'groupid',
|
||||
label: '分组ID',
|
||||
componentProps: {
|
||||
placeholder: '请输入分组ID',
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return formSchemas;
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<VbenDrawer
|
||||
v-model:show="isShow"
|
||||
:title="$t('channel.wechat.user.edit')"
|
||||
:loading="loading"
|
||||
width="600px"
|
||||
@confirm="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<VbenForm
|
||||
v-model:model="formModel"
|
||||
v-model:schema="formSchema"
|
||||
:label-width="100"
|
||||
@submit="handleConfirm"
|
||||
>
|
||||
<template #avatar="{ model, field }">
|
||||
<div class="flex items-center space-x-4">
|
||||
<img
|
||||
v-if="model.headimgurl"
|
||||
:src="model.headimgurl"
|
||||
class="w-16 h-16 rounded-full border-2 border-gray-200"
|
||||
alt="用户头像"
|
||||
/>
|
||||
<div v-if="model.nickname" class="text-sm text-gray-600">
|
||||
<div class="font-medium">{{ model.nickname }}</div>
|
||||
<div>OpenID: {{ model.openid }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useVbenForm, useVbenDrawer } from '@vben/common-ui';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { updateWechatUser } from '#/api/core/wechat';
|
||||
import type { WechatUser, WechatUserForm } from '../data';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
user?: WechatUser | null;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'success'): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const [VbenForm, formModel, formSchema] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'avatar',
|
||||
label: '用户信息',
|
||||
component: 'Slot',
|
||||
slot: 'avatar',
|
||||
formItemClass: 'mb-4',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注信息',
|
||||
rows: 3,
|
||||
maxlength: 200,
|
||||
showCount: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'groupid',
|
||||
label: '分组',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入分组ID',
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-1',
|
||||
});
|
||||
|
||||
const [VbenDrawer, isShow] = useVbenDrawer({
|
||||
formModel,
|
||||
formSchema,
|
||||
});
|
||||
|
||||
// 监听props变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
isShow.value = val;
|
||||
if (val && props.user) {
|
||||
// 初始化表单数据
|
||||
formModel.value = {
|
||||
id: props.user.id,
|
||||
remark: props.user.remark || '',
|
||||
groupid: props.user.groupid || 0,
|
||||
headimgurl: props.user.headimgurl,
|
||||
nickname: props.user.nickname,
|
||||
openid: props.user.openid,
|
||||
};
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(isShow, (val) => {
|
||||
emit('update:modelValue', val);
|
||||
});
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const data: WechatUserForm = {
|
||||
id: formModel.value.id,
|
||||
remark: formModel.value.remark,
|
||||
groupid: formModel.value.groupid,
|
||||
};
|
||||
|
||||
await updateWechatUser(data);
|
||||
message.success('用户信息更新成功');
|
||||
emit('success');
|
||||
isShow.value = false;
|
||||
} catch (error) {
|
||||
message.error('用户信息更新失败');
|
||||
console.error('更新用户失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
isShow.value = false;
|
||||
};
|
||||
</script>
|
||||
117
admin-vben/apps/web-antd/src/views/finance/payment/data.ts
Normal file
117
admin-vben/apps/web-antd/src/views/finance/payment/data.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { VxeGridProps } from '@vben/plugins/vxe-table';
|
||||
|
||||
export interface PaymentRecord {
|
||||
id: number;
|
||||
order_no: string;
|
||||
trade_no: string;
|
||||
user_id: number;
|
||||
username: string;
|
||||
amount: string;
|
||||
pay_type: string;
|
||||
pay_method: string;
|
||||
status: 'pending' | 'paid' | 'failed' | 'refunded' | 'closed';
|
||||
pay_time?: string;
|
||||
notify_time?: string;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
export interface PaymentForm {
|
||||
id?: number;
|
||||
order_no: string;
|
||||
trade_no: string;
|
||||
user_id: number;
|
||||
amount: string;
|
||||
pay_type: string;
|
||||
pay_method: string;
|
||||
status: string;
|
||||
pay_time?: string;
|
||||
notify_time?: string;
|
||||
}
|
||||
|
||||
export const payTypeOptions = [
|
||||
{ label: '微信支付', value: 'wechat' },
|
||||
{ label: '支付宝', value: 'alipay' },
|
||||
{ label: '银联', value: 'unionpay' },
|
||||
{ label: '余额支付', value: 'balance' },
|
||||
{ label: '其他', value: 'other' },
|
||||
];
|
||||
|
||||
export const statusOptions = [
|
||||
{ label: '待支付', value: 'pending' },
|
||||
{ label: '已支付', value: 'paid' },
|
||||
{ label: '支付失败', value: 'failed' },
|
||||
{ label: '已退款', value: 'refunded' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
];
|
||||
|
||||
export const statusColorMap = {
|
||||
pending: 'warning',
|
||||
paid: 'success',
|
||||
failed: 'error',
|
||||
refunded: 'default',
|
||||
closed: 'default',
|
||||
};
|
||||
|
||||
export const gridOptions: VxeGridProps<PaymentRecord> = {
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'order_no', title: '订单号', minWidth: 180 },
|
||||
{ field: 'trade_no', title: '交易号', minWidth: 180 },
|
||||
{ field: 'username', title: '用户', width: 120 },
|
||||
{ field: 'amount', title: '金额', width: 100, formatter: ({ cellValue }) => {
|
||||
return `¥${cellValue}`;
|
||||
}},
|
||||
{ field: 'pay_type', title: '支付类型', width: 100, formatter: ({ cellValue }) => {
|
||||
const option = payTypeOptions.find(item => item.value === cellValue);
|
||||
return option?.label || cellValue;
|
||||
}},
|
||||
{ field: 'pay_method', title: '支付方式', width: 120 },
|
||||
{ field: 'status', title: '状态', width: 100, formatter: ({ cellValue }) => {
|
||||
const colorMap = {
|
||||
pending: 'warning',
|
||||
paid: 'success',
|
||||
failed: 'error',
|
||||
refunded: 'default',
|
||||
closed: 'default',
|
||||
};
|
||||
const color = colorMap[cellValue] || 'default';
|
||||
const option = statusOptions.find(item => item.value === cellValue);
|
||||
return `<span class="ant-tag ant-tag-${color}">${option?.label || cellValue}</span>`;
|
||||
} },
|
||||
{ field: 'pay_time', title: '支付时间', width: 180 },
|
||||
{ field: 'create_time', title: '创建时间', width: 180 },
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: PaymentRecord) => {
|
||||
// This will be handled in the component
|
||||
},
|
||||
options: [
|
||||
{ code: 'view', text: '查看详情', icon: 'ant-design:eye-outlined' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
print: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
},
|
||||
};
|
||||
217
admin-vben/apps/web-antd/src/views/finance/payment/list.vue
Normal file
217
admin-vben/apps/web-antd/src/views/finance/payment/list.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<VbenVxeGrid
|
||||
ref="gridRef"
|
||||
:grid-options="gridOptions"
|
||||
:query-form-schema="queryFormSchema"
|
||||
@toolbar-button-click="handleToolbarButtonClick"
|
||||
>
|
||||
<template #status="{ row }">
|
||||
<VbenTag :color="statusColorMap[row.status]">
|
||||
{{ getStatusLabel(row.status) }}
|
||||
</VbenTag>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<VbenButton
|
||||
size="small"
|
||||
type="primary"
|
||||
variant="text"
|
||||
@click="handleView(row)"
|
||||
>
|
||||
详情
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
v-if="row.status === 'paid'"
|
||||
size="small"
|
||||
type="warning"
|
||||
variant="text"
|
||||
@click="handleRefund(row)"
|
||||
>
|
||||
退款
|
||||
</VbenButton>
|
||||
<VbenPopconfirm
|
||||
title="确定删除该记录吗?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<VbenButton
|
||||
size="small"
|
||||
type="danger"
|
||||
variant="text"
|
||||
>
|
||||
{{ $t('common.delete') }}
|
||||
</VbenButton>
|
||||
</VbenPopconfirm>
|
||||
</template>
|
||||
</VbenVxeGrid>
|
||||
|
||||
<PaymentDetailModal
|
||||
v-model:visible="detailModalVisible"
|
||||
:payment-data="viewingPayment"
|
||||
@cancel="handleDetailModalCancel"
|
||||
/>
|
||||
|
||||
<RefundModal
|
||||
v-model:visible="refundModalVisible"
|
||||
:payment-data="refundingPayment"
|
||||
@cancel="handleRefundModalCancel"
|
||||
@submit="handleRefundSubmit"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PaymentRecord } from './data';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useVbenVxeGrid, VbenButton, VbenPopconfirm, VbenTag, VbenVxeGrid } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { getPaymentListApi, deletePaymentApi, refundPaymentApi } from '#/api/core/finance';
|
||||
import { SvgIcon } from '#/components/icon';
|
||||
|
||||
import PaymentDetailModal from './modules/detail.vue';
|
||||
import RefundModal from './modules/refund.vue';
|
||||
import { gridOptions, payTypeOptions, statusOptions, statusColorMap } from './data';
|
||||
|
||||
const gridRef = ref();
|
||||
const detailModalVisible = ref(false);
|
||||
const refundModalVisible = ref(false);
|
||||
const viewingPayment = ref<any>(null);
|
||||
const refundingPayment = ref<any>(null);
|
||||
|
||||
const queryFormSchema = computed(() => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'order_no',
|
||||
label: '订单号',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'trade_no',
|
||||
label: '交易号',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'username',
|
||||
label: '用户',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'pay_type',
|
||||
label: '支付类型',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
...payTypeOptions,
|
||||
],
|
||||
placeholder: '请选择支付类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
...statusOptions,
|
||||
],
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'DateRange',
|
||||
fieldName: 'create_time',
|
||||
label: '创建时间',
|
||||
componentProps: {
|
||||
placeholder: ['开始时间', '结束时间'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
queryFormSchema,
|
||||
});
|
||||
|
||||
function handleToolbarButtonClick(event: string) {
|
||||
switch (event) {
|
||||
case 'add':
|
||||
handleAdd();
|
||||
break;
|
||||
case 'refresh':
|
||||
handleRefresh();
|
||||
break;
|
||||
case 'export':
|
||||
handleExport();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
// Payment records are generated automatically, cannot manually add
|
||||
$message.info('支付记录自动生成,无法手动添加');
|
||||
}
|
||||
|
||||
function handleView(row: PaymentRecord) {
|
||||
viewingPayment.value = row;
|
||||
detailModalVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleRefund(row: PaymentRecord) {
|
||||
refundingPayment.value = row;
|
||||
refundModalVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleDelete(row: PaymentRecord) {
|
||||
try {
|
||||
await deletePaymentApi(row.id);
|
||||
await handleRefresh();
|
||||
$message.success('删除成功');
|
||||
} catch (error) {
|
||||
$message.error('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
function handleDetailModalCancel() {
|
||||
detailModalVisible.value = false;
|
||||
viewingPayment.value = null;
|
||||
}
|
||||
|
||||
function handleRefundModalCancel() {
|
||||
refundModalVisible.value = false;
|
||||
refundingPayment.value = null;
|
||||
}
|
||||
|
||||
async function handleRefundSubmit(refundData: any) {
|
||||
try {
|
||||
await refundPaymentApi(refundingPayment.value.id, refundData);
|
||||
refundModalVisible.value = false;
|
||||
await handleRefresh();
|
||||
$message.success('退款成功');
|
||||
} catch (error) {
|
||||
$message.error('退款失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusLabel(status: string): string {
|
||||
const option = statusOptions.find(item => item.value === status);
|
||||
return option?.label || status;
|
||||
}
|
||||
|
||||
async function handleRefresh() {
|
||||
await gridApi.query();
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
gridApi.exportData({
|
||||
filename: '支付记录列表',
|
||||
type: 'csv',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-medium mb-4">支付详情</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="font-medium mb-2">订单信息</h4>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div><strong>订单号:</strong> {{ paymentData.order_no }}</div>
|
||||
<div><strong>交易号:</strong> {{ paymentData.trade_no }}</div>
|
||||
<div><strong>用户:</strong> {{ paymentData.username }}</div>
|
||||
<div><strong>金额:</strong> <span class="text-red-600">¥{{ paymentData.amount }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="font-medium mb-2">支付信息</h4>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div><strong>支付类型:</strong> {{ getPayTypeLabel(paymentData.pay_type) }}</div>
|
||||
<div><strong>支付方式:</strong> {{ paymentData.pay_method }}</div>
|
||||
<div><strong>状态:</strong> <VbenTag :color="statusColorMap[paymentData.status]">{{ getStatusLabel(paymentData.status) }}</VbenTag></div>
|
||||
<div><strong>支付时间:</strong> {{ paymentData.pay_time || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h4 class="font-medium mb-2">时间记录</h4>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div><strong>创建时间:</strong> {{ paymentData.create_time }}</div>
|
||||
<div><strong>更新时间:</strong> {{ paymentData.update_time }}</div>
|
||||
<div><strong>通知时间:</strong> {{ paymentData.notify_time || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-2">
|
||||
<VbenButton @click="handleClose" variant="outline">
|
||||
{{ $t('common.close') }}
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
v-if="paymentData.status === 'paid'"
|
||||
type="warning"
|
||||
@click="handleRefund"
|
||||
>
|
||||
退款
|
||||
</VbenButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { VbenButton, VbenTag } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
interface Props {
|
||||
paymentData: any;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'cancel'): void;
|
||||
(e: 'refund'): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const payTypeOptions = [
|
||||
{ label: '微信支付', value: 'wechat' },
|
||||
{ label: '支付宝', value: 'alipay' },
|
||||
{ label: '银联', value: 'unionpay' },
|
||||
{ label: '余额支付', value: 'balance' },
|
||||
{ label: '其他', value: 'other' },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '待支付', value: 'pending' },
|
||||
{ label: '已支付', value: 'paid' },
|
||||
{ label: '支付失败', value: 'failed' },
|
||||
{ label: '已退款', value: 'refunded' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
];
|
||||
|
||||
const statusColorMap = {
|
||||
pending: 'warning',
|
||||
paid: 'success',
|
||||
failed: 'error',
|
||||
refunded: 'default',
|
||||
closed: 'default',
|
||||
};
|
||||
|
||||
function getPayTypeLabel(type: string): string {
|
||||
const option = payTypeOptions.find(item => item.value === type);
|
||||
return option?.label || type;
|
||||
}
|
||||
|
||||
function getStatusLabel(status: string): string {
|
||||
const option = statusOptions.find(item => item.value === status);
|
||||
return option?.label || status;
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
function handleRefund() {
|
||||
emit('refund');
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-medium mb-4">退款操作</h3>
|
||||
|
||||
<VbenForm
|
||||
:handle-submit="handleSubmit"
|
||||
:model="model"
|
||||
:schema="formSchemas"
|
||||
:show-default-actions="false"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<template #form-submit>
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<VbenButton @click="handleCancel" variant="outline">
|
||||
{{ $t('common.cancel') }}
|
||||
</VbenButton>
|
||||
<VbenButton type="primary" @click="handleSubmit">
|
||||
确认退款
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { VbenButton, VbenForm, useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
interface Props {
|
||||
paymentData: any;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'cancel'): void;
|
||||
(e: 'submit', data: any): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const [Modal] = useVbenModal();
|
||||
const model = ref({
|
||||
refund_amount: props.paymentData?.amount || '',
|
||||
refund_reason: '',
|
||||
notify_url: '',
|
||||
});
|
||||
|
||||
const formSchemas = computed(() => [
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'refund_amount',
|
||||
label: '退款金额',
|
||||
rules: 'required|min:0.01|max:' + props.paymentData?.amount,
|
||||
componentProps: {
|
||||
placeholder: '请输入退款金额',
|
||||
min: 0.01,
|
||||
max: parseFloat(props.paymentData?.amount || '0'),
|
||||
step: 0.01,
|
||||
precision: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'refund_reason',
|
||||
label: '退款原因',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入退款原因',
|
||||
rows: 3,
|
||||
maxlength: 200,
|
||||
showCount: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'notify_url',
|
||||
label: '通知地址',
|
||||
componentProps: {
|
||||
placeholder: '请输入退款通知地址(可选)',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await Modal?.formApi.validate();
|
||||
const formValues = Modal?.formApi.getValues() || model.value;
|
||||
emit('submit', formValues);
|
||||
} catch (error) {
|
||||
console.error('Form validation failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('cancel');
|
||||
}
|
||||
</script>
|
||||
103
admin-vben/apps/web-antd/src/views/log/admin/data.ts
Normal file
103
admin-vben/apps/web-antd/src/views/log/admin/data.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { VxeGridProps } from '@vben/plugins/vxe-table';
|
||||
|
||||
export interface AdminLog {
|
||||
id: number;
|
||||
admin_id: number;
|
||||
admin_name: string;
|
||||
module: string;
|
||||
controller: string;
|
||||
action: string;
|
||||
method: string;
|
||||
url: string;
|
||||
params: string;
|
||||
ip: string;
|
||||
user_agent: string;
|
||||
result: 'success' | 'failed';
|
||||
message?: string;
|
||||
create_time: string;
|
||||
}
|
||||
|
||||
export interface LogForm {
|
||||
id?: number;
|
||||
admin_id: number;
|
||||
admin_name: string;
|
||||
module: string;
|
||||
controller: string;
|
||||
action: string;
|
||||
method: string;
|
||||
url: string;
|
||||
params: string;
|
||||
ip: string;
|
||||
user_agent: string;
|
||||
result: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const resultOptions = [
|
||||
{ label: '成功', value: 'success' },
|
||||
{ label: '失败', value: 'failed' },
|
||||
];
|
||||
|
||||
export const methodOptions = [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
{ label: 'PATCH', value: 'PATCH' },
|
||||
];
|
||||
|
||||
export const resultColorMap = {
|
||||
success: 'success',
|
||||
failed: 'error',
|
||||
};
|
||||
|
||||
export const gridOptions: VxeGridProps<AdminLog> = {
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'admin_name', title: '管理员', width: 120 },
|
||||
{ field: 'module', title: '模块', width: 100 },
|
||||
{ field: 'controller', title: '控制器', width: 120 },
|
||||
{ field: 'action', title: '操作', width: 100 },
|
||||
{ field: 'method', title: '方法', width: 80 },
|
||||
{ field: 'url', title: 'URL', minWidth: 200, showOverflow: true },
|
||||
{ field: 'ip', title: 'IP地址', width: 120 },
|
||||
{ field: 'result', title: '结果', width: 80, formatter: ({ cellValue }) => {
|
||||
const colorMap = { success: 'success', failed: 'error' };
|
||||
const color = colorMap[cellValue] || 'default';
|
||||
return `<span class="ant-tag ant-tag-${color}">${cellValue === 'success' ? '成功' : '失败'}</span>`;
|
||||
} },
|
||||
{ field: 'create_time', title: '操作时间', width: 180 },
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
title: '操作',
|
||||
width: 100,
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: AdminLog) => {
|
||||
// This will be handled in the component
|
||||
},
|
||||
options: [
|
||||
{ code: 'view', text: '查看详情', icon: 'ant-design:eye-outlined' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
print: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
},
|
||||
};
|
||||
187
admin-vben/apps/web-antd/src/views/log/admin/list.vue
Normal file
187
admin-vben/apps/web-antd/src/views/log/admin/list.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<VbenVxeGrid
|
||||
ref="gridRef"
|
||||
:grid-options="gridOptions"
|
||||
:query-form-schema="queryFormSchema"
|
||||
@toolbar-button-click="handleToolbarButtonClick"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<VbenButton type="danger" @click="handleClearLogs">
|
||||
<SvgIcon icon="mdi:delete-sweep" class="mr-1" />
|
||||
清空日志
|
||||
</VbenButton>
|
||||
</template>
|
||||
|
||||
<template #result="{ row }">
|
||||
<VbenTag :color="resultColorMap[row.result]">
|
||||
{{ getResultLabel(row.result) }}
|
||||
</VbenTag>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<VbenButton
|
||||
size="small"
|
||||
type="primary"
|
||||
variant="text"
|
||||
@click="handleView(row)"
|
||||
>
|
||||
详情
|
||||
</VbenButton>
|
||||
</template>
|
||||
</VbenVxeGrid>
|
||||
|
||||
<LogDetailModal
|
||||
v-model:visible="detailModalVisible"
|
||||
:log-data="viewingLog"
|
||||
@cancel="handleDetailModalCancel"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { AdminLog } from './data';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useVbenVxeGrid, VbenButton, VbenTag, VbenVxeGrid } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { getAdminLogListApi, deleteAdminLogApi, clearAdminLogApi } from '#/api/core/log';
|
||||
import { SvgIcon } from '#/components/icon';
|
||||
|
||||
import LogDetailModal from './modules/detail.vue';
|
||||
import { gridOptions, resultOptions, resultColorMap } from './data';
|
||||
|
||||
const gridRef = ref();
|
||||
const detailModalVisible = ref(false);
|
||||
const viewingLog = ref<any>(null);
|
||||
|
||||
const queryFormSchema = computed(() => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'admin_name',
|
||||
label: '管理员',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'module',
|
||||
label: '模块',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'controller',
|
||||
label: '控制器',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'action',
|
||||
label: '操作',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'method',
|
||||
label: '方法',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
{ label: 'PATCH', value: 'PATCH' },
|
||||
],
|
||||
placeholder: '请选择请求方法',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'ip',
|
||||
label: 'IP地址',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'result',
|
||||
label: '结果',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '成功', value: 'success' },
|
||||
{ label: '失败', value: 'failed' },
|
||||
],
|
||||
placeholder: '请选择操作结果',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'DateRange',
|
||||
fieldName: 'create_time',
|
||||
label: '操作时间',
|
||||
componentProps: {
|
||||
placeholder: ['开始时间', '结束时间'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
queryFormSchema,
|
||||
});
|
||||
|
||||
function handleToolbarButtonClick(event: string) {
|
||||
switch (event) {
|
||||
case 'add':
|
||||
handleAdd();
|
||||
break;
|
||||
case 'refresh':
|
||||
handleRefresh();
|
||||
break;
|
||||
case 'export':
|
||||
handleExport();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
// Admin logs are generated automatically, cannot manually add
|
||||
$message.info('管理员日志自动生成,无法手动添加');
|
||||
}
|
||||
|
||||
function handleView(row: AdminLog) {
|
||||
viewingLog.value = row;
|
||||
detailModalVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleClearLogs() {
|
||||
try {
|
||||
await clearAdminLogApi();
|
||||
await handleRefresh();
|
||||
$message.success('日志清空成功');
|
||||
} catch (error) {
|
||||
$message.error('日志清空失败');
|
||||
}
|
||||
}
|
||||
|
||||
function handleDetailModalCancel() {
|
||||
detailModalVisible.value = false;
|
||||
viewingLog.value = null;
|
||||
}
|
||||
|
||||
function getResultLabel(result: string): string {
|
||||
const option = resultOptions.find(item => item.value === result);
|
||||
return option?.label || result;
|
||||
}
|
||||
|
||||
async function handleRefresh() {
|
||||
await gridApi.query();
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
gridApi.exportData({
|
||||
filename: '管理员日志列表',
|
||||
type: 'csv',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
110
admin-vben/apps/web-antd/src/views/log/admin/modules/detail.vue
Normal file
110
admin-vben/apps/web-antd/src/views/log/admin/modules/detail.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-medium mb-4">日志详情</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="font-medium mb-2">管理员信息</h4>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div><strong>管理员ID:</strong> {{ logData.admin_id }}</div>
|
||||
<div><strong>管理员名称:</strong> {{ logData.admin_name }}</div>
|
||||
<div><strong>IP地址:</strong> {{ logData.ip }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="font-medium mb-2">操作信息</h4>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div><strong>模块:</strong> {{ logData.module }}</div>
|
||||
<div><strong>控制器:</strong> {{ logData.controller }}</div>
|
||||
<div><strong>操作:</strong> {{ logData.action }}</div>
|
||||
<div><strong>方法:</strong> {{ logData.method }}</div>
|
||||
<div><strong>结果:</strong> <VbenTag :color="resultColorMap[logData.result]">{{ getResultLabel(logData.result) }}</VbenTag></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h4 class="font-medium mb-2">请求信息</h4>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="space-y-2 text-sm">
|
||||
<div><strong>URL:</strong> {{ logData.url }}</div>
|
||||
<div><strong>UserAgent:</strong> {{ logData.user_agent }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h4 class="font-medium mb-2">参数信息</h4>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<pre class="text-sm whitespace-pre-wrap">{{ formatParams(logData.params) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="logData.message" class="mb-6">
|
||||
<h4 class="font-medium mb-2">消息</h4>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<pre class="text-sm whitespace-pre-wrap">{{ logData.message }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h4 class="font-medium mb-2">时间记录</h4>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="text-sm">
|
||||
<div><strong>操作时间:</strong> {{ logData.create_time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<VbenButton @click="handleClose" variant="outline">
|
||||
{{ $t('common.close') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { VbenButton, VbenTag } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
interface Props {
|
||||
logData: any;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const resultOptions = [
|
||||
{ label: '成功', value: 'success' },
|
||||
{ label: '失败', value: 'failed' },
|
||||
];
|
||||
|
||||
const resultColorMap = {
|
||||
success: 'success',
|
||||
failed: 'error',
|
||||
};
|
||||
|
||||
function getResultLabel(result: string): string {
|
||||
const option = resultOptions.find(item => item.value === result);
|
||||
return option?.label || result;
|
||||
}
|
||||
|
||||
function formatParams(params: string): string {
|
||||
try {
|
||||
const parsed = JSON.parse(params);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('cancel');
|
||||
}
|
||||
</script>
|
||||
@@ -87,17 +87,23 @@ export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ field: 'payment_name', title: '支付名称', minWidth: 150 },
|
||||
{ field: 'payment_type', title: '支付类型', width: 120, slots: { default: 'paymentType' } },
|
||||
{ field: 'payment_type', title: '支付类型', width: 120, formatter: ({ cellValue }) => {
|
||||
return paymentTypeMap[cellValue] || cellValue;
|
||||
} },
|
||||
{ field: 'payment_code', title: '支付编码', width: 150 },
|
||||
{
|
||||
field: 'icon',
|
||||
title: '图标',
|
||||
width: 100,
|
||||
slots: { default: 'icon' },
|
||||
align: 'center',
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue ? `<i class="${cellValue}" style="font-size: 24px;"></i>` : '';
|
||||
},
|
||||
},
|
||||
{ field: 'sort', title: '排序', width: 80 },
|
||||
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } },
|
||||
{ field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '启用' : '禁用';
|
||||
} },
|
||||
{ field: 'create_time', title: '创建时间', width: 180 },
|
||||
{ field: 'update_time', title: '更新时间', width: 180 },
|
||||
{
|
||||
@@ -105,6 +111,18 @@ export const columns: VxeGridProps['columns'] = [
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: PaymentItem) => {
|
||||
// This will be handled in the component
|
||||
},
|
||||
options: [
|
||||
{ code: 'edit', text: '编辑', icon: 'ant-design:edit-outlined' },
|
||||
{ code: 'config', text: '配置', icon: 'ant-design:setting-outlined' },
|
||||
{ code: 'delete', text: '删除', icon: 'ant-design:delete-outlined', danger: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -91,11 +91,15 @@ export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ field: 'sms_name', title: '短信名称', minWidth: 150 },
|
||||
{ field: 'sms_type', title: '短信类型', width: 120, slots: { default: 'smsType' } },
|
||||
{ field: 'sms_type', title: '短信类型', width: 120, formatter: ({ cellValue }) => {
|
||||
return smsTypeMap[cellValue] || cellValue;
|
||||
} },
|
||||
{ field: 'sms_code', title: '短信编码', width: 150 },
|
||||
{ field: 'sign_name', title: '签名名称', width: 150 },
|
||||
{ field: 'sort', title: '排序', width: 80 },
|
||||
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } },
|
||||
{ field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '启用' : '禁用';
|
||||
} },
|
||||
{ field: 'create_time', title: '创建时间', width: 180 },
|
||||
{ field: 'update_time', title: '更新时间', width: 180 },
|
||||
{
|
||||
@@ -103,6 +107,18 @@ export const columns: VxeGridProps['columns'] = [
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: SmsItem) => {
|
||||
// This will be handled in the component
|
||||
},
|
||||
options: [
|
||||
{ code: 'edit', text: '编辑', icon: 'ant-design:edit-outlined' },
|
||||
{ code: 'test', text: '测试', icon: 'ant-design:notification-outlined' },
|
||||
{ code: 'delete', text: '删除', icon: 'ant-design:delete-outlined', danger: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -105,11 +105,17 @@ export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ field: 'storage_name', title: '存储名称', minWidth: 150 },
|
||||
{ field: 'storage_type', title: '存储类型', width: 120, slots: { default: 'storageType' } },
|
||||
{ field: 'storage_type', title: '存储类型', width: 120, formatter: ({ cellValue }) => {
|
||||
return storageTypeMap[cellValue] || cellValue;
|
||||
} },
|
||||
{ field: 'storage_code', title: '存储编码', width: 150 },
|
||||
{ field: 'is_default', title: '默认存储', width: 100, slots: { default: 'isDefault' } },
|
||||
{ field: 'is_default', title: '默认存储', width: 100, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '是' : '否';
|
||||
} },
|
||||
{ field: 'sort', title: '排序', width: 80 },
|
||||
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } },
|
||||
{ field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '启用' : '禁用';
|
||||
} },
|
||||
{ field: 'create_time', title: '创建时间', width: 180 },
|
||||
{ field: 'update_time', title: '更新时间', width: 180 },
|
||||
{
|
||||
@@ -117,6 +123,18 @@ export const columns: VxeGridProps['columns'] = [
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: StorageItem) => {
|
||||
// This will be handled in the component
|
||||
},
|
||||
options: [
|
||||
{ code: 'edit', text: '编辑', icon: 'ant-design:edit-outlined' },
|
||||
{ code: 'setDefault', text: '设为默认', icon: 'ant-design:star-outlined' },
|
||||
{ code: 'delete', text: '删除', icon: 'ant-design:delete-outlined', danger: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -91,12 +91,19 @@ export const querySchema = [
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ field: 'app_module', title: '模块', width: 120, slots: { default: 'module' } },
|
||||
{ field: 'app_module', title: '模块', width: 120, formatter: ({ cellValue }) => {
|
||||
const option = moduleOptions.find(item => item.value === cellValue);
|
||||
return option?.label || cellValue;
|
||||
} },
|
||||
{ field: 'config_key', title: '配置键', width: 200 },
|
||||
{ field: 'config_value', title: '配置值', minWidth: 200, showOverflow: true },
|
||||
{ field: 'config_desc', title: '配置描述', minWidth: 200, showOverflow: true },
|
||||
{ field: 'is_system', title: '系统配置', width: 100, slots: { default: 'isSystem' } },
|
||||
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } },
|
||||
{ field: 'is_system', title: '系统配置', width: 100, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '是' : '否';
|
||||
} },
|
||||
{ field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '启用' : '禁用';
|
||||
} },
|
||||
{ field: 'create_time', title: '创建时间', width: 180 },
|
||||
{ field: 'update_time', title: '更新时间', width: 180 },
|
||||
{
|
||||
@@ -104,6 +111,17 @@ export const columns: VxeGridProps['columns'] = [
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: ConfigItem) => {
|
||||
// This will be handled in the component
|
||||
},
|
||||
options: [
|
||||
{ code: 'edit', text: '编辑', icon: 'ant-design:edit-outlined' },
|
||||
{ code: 'delete', text: '删除', icon: 'ant-design:delete-outlined', danger: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
116
admin-vben/apps/web-antd/src/views/setting/system/data.ts
Normal file
116
admin-vben/apps/web-antd/src/views/setting/system/data.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { VxeGridProps } from '@vben/plugins/vxe-table';
|
||||
|
||||
export interface SystemConfig {
|
||||
id: number;
|
||||
site_id: number;
|
||||
name: string;
|
||||
title: string;
|
||||
value: string;
|
||||
type: 'text' | 'textarea' | 'number' | 'date' | 'datetime' | 'select' | 'radio' | 'checkbox' | 'image' | 'file' | 'color' | 'array' | 'json';
|
||||
options?: string;
|
||||
tips?: string;
|
||||
group: string;
|
||||
sort: number;
|
||||
status: 0 | 1;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
export interface ConfigForm {
|
||||
id?: number;
|
||||
site_id: number;
|
||||
name: string;
|
||||
title: string;
|
||||
value: string;
|
||||
type: string;
|
||||
options?: string;
|
||||
tips?: string;
|
||||
group: string;
|
||||
sort: number;
|
||||
status: 0 | 1;
|
||||
}
|
||||
|
||||
export const typeOptions = [
|
||||
{ label: '文本框', value: 'text' },
|
||||
{ label: '文本域', value: 'textarea' },
|
||||
{ label: '数字', value: 'number' },
|
||||
{ label: '日期', value: 'date' },
|
||||
{ label: '日期时间', value: 'datetime' },
|
||||
{ label: '下拉框', value: 'select' },
|
||||
{ label: '单选框', value: 'radio' },
|
||||
{ label: '复选框', value: 'checkbox' },
|
||||
{ label: '图片上传', value: 'image' },
|
||||
{ label: '文件上传', value: 'file' },
|
||||
{ label: '颜色选择', value: 'color' },
|
||||
{ label: '数组', value: 'array' },
|
||||
{ label: 'JSON', value: 'json' },
|
||||
];
|
||||
|
||||
export const statusOptions = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
];
|
||||
|
||||
export const groupOptions = [
|
||||
{ label: '站点配置', value: 'site' },
|
||||
{ label: '系统配置', value: 'system' },
|
||||
{ label: '上传配置', value: 'upload' },
|
||||
{ label: '邮件配置', value: 'email' },
|
||||
{ label: '短信配置', value: 'sms' },
|
||||
{ label: '支付配置', value: 'payment' },
|
||||
{ label: '其他配置', value: 'other' },
|
||||
];
|
||||
|
||||
export const gridOptions: VxeGridProps<SystemConfig> = {
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'name', title: '配置名称', minWidth: 150 },
|
||||
{ field: 'title', title: '配置标题', minWidth: 150 },
|
||||
{ field: 'type', title: '类型', width: 100, formatter: ({ cellValue }) => {
|
||||
const option = typeOptions.find(item => item.value === cellValue);
|
||||
return option?.label || cellValue;
|
||||
}},
|
||||
{ field: 'group', title: '分组', width: 100, formatter: ({ cellValue }) => {
|
||||
const option = groupOptions.find(item => item.value === cellValue);
|
||||
return option?.label || cellValue;
|
||||
}},
|
||||
{ field: 'value', title: '配置值', minWidth: 200, showOverflow: true },
|
||||
{ field: 'sort', title: '排序', width: 80 },
|
||||
{ field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '启用' : '禁用';
|
||||
}},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: SystemConfig) => {
|
||||
// This will be handled in the component
|
||||
},
|
||||
options: [
|
||||
{ code: 'edit', text: '编辑', icon: 'ant-design:edit-outlined' },
|
||||
{ code: 'delete', text: '删除', icon: 'ant-design:delete-outlined', danger: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
print: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
},
|
||||
};
|
||||
204
admin-vben/apps/web-antd/src/views/setting/system/list.vue
Normal file
204
admin-vben/apps/web-antd/src/views/setting/system/list.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<VbenVxeGrid
|
||||
ref="gridRef"
|
||||
:grid-options="gridOptions"
|
||||
:query-form-schema="queryFormSchema"
|
||||
@toolbar-button-click="handleToolbarButtonClick"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<VbenButton type="primary" @click="handleRefreshCache">
|
||||
<SvgIcon icon="mdi:refresh" class="mr-1" />
|
||||
刷新缓存
|
||||
</VbenButton>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<VbenButton
|
||||
size="small"
|
||||
type="primary"
|
||||
variant="text"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
{{ $t('common.edit') }}
|
||||
</VbenButton>
|
||||
<VbenPopconfirm
|
||||
title="确定删除该配置吗?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<VbenButton
|
||||
size="small"
|
||||
type="danger"
|
||||
variant="text"
|
||||
>
|
||||
{{ $t('common.delete') }}
|
||||
</VbenButton>
|
||||
</VbenPopconfirm>
|
||||
</template>
|
||||
</VbenVxeGrid>
|
||||
|
||||
<ConfigFormModal
|
||||
v-model:visible="modalVisible"
|
||||
:id="editingId"
|
||||
@cancel="handleModalCancel"
|
||||
@submit="handleModalSubmit"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SystemConfig, ConfigForm } from './data';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useVbenVxeGrid, VbenButton, VbenPopconfirm, VbenVxeGrid } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { getSystemConfigListApi, createSystemConfigApi, updateSystemConfigApi, deleteSystemConfigApi, refreshSystemConfigCacheApi } from '#/api/core/system';
|
||||
import { SvgIcon } from '#/components/icon';
|
||||
|
||||
import ConfigFormModal from './modules/form.vue';
|
||||
import { gridOptions } from './data';
|
||||
|
||||
const gridRef = ref();
|
||||
const modalVisible = ref(false);
|
||||
const editingId = ref<number | undefined>();
|
||||
|
||||
const queryFormSchema = computed(() => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '配置名称',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'title',
|
||||
label: '配置标题',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'group',
|
||||
label: '配置分组',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '站点配置', value: 'site' },
|
||||
{ label: '系统配置', value: 'system' },
|
||||
{ label: '上传配置', value: 'upload' },
|
||||
{ label: '邮件配置', value: 'email' },
|
||||
{ label: '短信配置', value: 'sms' },
|
||||
{ label: '支付配置', value: 'payment' },
|
||||
{ label: '其他配置', value: 'other' },
|
||||
],
|
||||
placeholder: '请选择配置分组',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'type',
|
||||
label: '配置类型',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '文本框', value: 'text' },
|
||||
{ label: '文本域', value: 'textarea' },
|
||||
{ label: '数字', value: 'number' },
|
||||
{ label: '日期', value: 'date' },
|
||||
{ label: '日期时间', value: 'datetime' },
|
||||
{ label: '下拉框', value: 'select' },
|
||||
{ label: '单选框', value: 'radio' },
|
||||
{ label: '复选框', value: 'checkbox' },
|
||||
{ label: '图片上传', value: 'image' },
|
||||
{ label: '文件上传', value: 'file' },
|
||||
{ label: '颜色选择', value: 'color' },
|
||||
{ label: '数组', value: 'array' },
|
||||
{ label: 'JSON', value: 'json' },
|
||||
],
|
||||
placeholder: '请选择配置类型',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
queryFormSchema,
|
||||
});
|
||||
|
||||
function handleToolbarButtonClick(event: string) {
|
||||
switch (event) {
|
||||
case 'add':
|
||||
handleAdd();
|
||||
break;
|
||||
case 'refresh':
|
||||
handleRefresh();
|
||||
break;
|
||||
case 'export':
|
||||
handleExport();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
editingId.value = undefined;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(row: SystemConfig) {
|
||||
editingId.value = row.id;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleDelete(row: SystemConfig) {
|
||||
try {
|
||||
await deleteSystemConfigApi(row.id);
|
||||
await handleRefresh();
|
||||
$message.success('删除成功');
|
||||
} catch (error) {
|
||||
$message.error('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRefreshCache() {
|
||||
try {
|
||||
await refreshSystemConfigCacheApi();
|
||||
$message.success('缓存刷新成功');
|
||||
} catch (error) {
|
||||
$message.error('缓存刷新失败');
|
||||
}
|
||||
}
|
||||
|
||||
function handleModalCancel() {
|
||||
modalVisible.value = false;
|
||||
editingId.value = undefined;
|
||||
}
|
||||
|
||||
async function handleModalSubmit(data: ConfigForm) {
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await updateSystemConfigApi(editingId.value, data);
|
||||
$message.success('更新成功');
|
||||
} else {
|
||||
await createSystemConfigApi(data);
|
||||
$message.success('创建成功');
|
||||
}
|
||||
modalVisible.value = false;
|
||||
await handleRefresh();
|
||||
} catch (error) {
|
||||
$message.error(editingId.value ? '更新失败' : '创建失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRefresh() {
|
||||
await gridApi.query();
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
gridApi.exportData({
|
||||
filename: '系统配置列表',
|
||||
type: 'csv',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div>
|
||||
<VbenForm
|
||||
:handle-submit="handleSubmit"
|
||||
:model="model"
|
||||
:schema="formSchemas"
|
||||
:show-default-actions="false"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<template #form-submit>
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<VbenButton @click="handleCancel" variant="outline">
|
||||
{{ $t('common.cancel') }}
|
||||
</VbenButton>
|
||||
<VbenButton type="primary" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ConfigForm } from '../data';
|
||||
|
||||
import { VbenButton, VbenForm, useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { useConfigFormSchemas } from './formSchemas';
|
||||
|
||||
interface Props {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', data: ConfigForm): void;
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
id: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const [Drawer] = useVbenDrawer();
|
||||
const model = ref<ConfigForm>({
|
||||
site_id: 0,
|
||||
name: '',
|
||||
title: '',
|
||||
type: 'text',
|
||||
value: '',
|
||||
group: 'other',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const formSchemas = useConfigFormSchemas();
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await Drawer?.formApi.validate();
|
||||
const formValues = Drawer?.formApi.getValues() || model.value;
|
||||
emit('submit', formValues);
|
||||
} catch (error) {
|
||||
console.error('Form validation failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
// Load config data if editing
|
||||
onMounted(async () => {
|
||||
if (props.id) {
|
||||
try {
|
||||
// Load config data
|
||||
const configData = await getSystemConfigDetailApi(props.id);
|
||||
model.value = { ...configData };
|
||||
} catch (error) {
|
||||
console.error('Failed to load config data:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { ConfigForm } from '../data';
|
||||
|
||||
import { useVbenForm } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { typeOptions, statusOptions, groupOptions } from '../data';
|
||||
|
||||
export const useConfigFormSchemas = () => {
|
||||
const formSchemas = computed(() => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '配置名称',
|
||||
rules: 'required|pattern:^[a-zA-Z_][a-zA-Z0-9_]*$',
|
||||
componentProps: {
|
||||
placeholder: '请输入配置名称(英文)',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'title',
|
||||
label: '配置标题',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入配置标题',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'type',
|
||||
label: '配置类型',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: typeOptions,
|
||||
placeholder: '请选择配置类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'group',
|
||||
label: '配置分组',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: groupOptions,
|
||||
placeholder: '请选择配置分组',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'options',
|
||||
label: '配置选项',
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return ['select', 'radio', 'checkbox'].includes(form.type);
|
||||
}),
|
||||
componentProps: {
|
||||
placeholder: '请输入配置选项,格式:key:value,每行一个',
|
||||
rows: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'tips',
|
||||
label: '配置说明',
|
||||
componentProps: {
|
||||
placeholder: '请输入配置说明',
|
||||
rows: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'sort',
|
||||
label: '排序',
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
max: 999,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
defaultValue: 1,
|
||||
componentProps: {
|
||||
options: statusOptions,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return formSchemas;
|
||||
};
|
||||
@@ -195,77 +195,47 @@ export function useColumns(
|
||||
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>
|
||||
),
|
||||
formatter: ({ row }) => {
|
||||
const colorMap: Record<number, string> = {
|
||||
0: 'red',
|
||||
1: 'green',
|
||||
2: 'orange',
|
||||
3: 'red',
|
||||
};
|
||||
const color = colorMap[row.status] || 'default';
|
||||
return `<span class="ant-tag ant-tag-${color} cursor-pointer" onclick="window.handleStatusChange(${row.status}, ${row.site_id})">${row.status_name}</span>`;
|
||||
},
|
||||
},
|
||||
{
|
||||
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>
|
||||
),
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: any) => {
|
||||
if (code === 'toSite') {
|
||||
window.open(`/site/${row.site_id}`, '_blank');
|
||||
} else if (code === 'toggleStatus') {
|
||||
onOpenClose(row.status, row.site_id);
|
||||
} else if (code === 'info') {
|
||||
onActionClick('info', row);
|
||||
} else if (code === 'init') {
|
||||
onActionClick('init', row);
|
||||
} else if (code === 'edit') {
|
||||
onActionClick('edit', row);
|
||||
} else if (code === 'delete') {
|
||||
onActionClick('delete', row);
|
||||
}
|
||||
},
|
||||
options: [
|
||||
{ code: 'toSite', text: $t('site.list.toSite'), icon: 'ant-design:login-outlined' },
|
||||
{ code: 'info', text: $t('site.list.info'), icon: 'ant-design:info-circle-outlined' },
|
||||
{ code: 'init', text: $t('site.list.initSite'), icon: 'ant-design:reload-outlined' },
|
||||
{ code: 'edit', text: $t('common.edit'), icon: 'ant-design:edit-outlined' },
|
||||
{ code: 'delete', text: $t('common.delete'), icon: 'ant-design:delete-outlined', danger: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -29,10 +29,12 @@ export function useColumns(
|
||||
align: 'left',
|
||||
field: 'meta.title',
|
||||
fixed: 'left',
|
||||
slots: { default: 'title' },
|
||||
title: $t('system.menu.menuTitle'),
|
||||
treeNode: true,
|
||||
width: 250,
|
||||
formatter: ({ row }) => {
|
||||
return row.meta?.title || '';
|
||||
},
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
|
||||
99
admin-vben/apps/web-antd/src/views/tools/backup/data.ts
Normal file
99
admin-vben/apps/web-antd/src/views/tools/backup/data.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { VxeGridProps } from '@vben/plugins/vxe-table';
|
||||
|
||||
export interface BackupInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'database' | 'file' | 'full';
|
||||
size: string;
|
||||
path: string;
|
||||
status: 'success' | 'failed' | 'running';
|
||||
start_time: string;
|
||||
end_time?: string;
|
||||
create_time: string;
|
||||
}
|
||||
|
||||
export interface BackupForm {
|
||||
id?: number;
|
||||
name: string;
|
||||
type: string;
|
||||
tables?: string[];
|
||||
exclude_tables?: string[];
|
||||
compress: 0 | 1;
|
||||
}
|
||||
|
||||
export const typeOptions = [
|
||||
{ label: '数据库备份', value: 'database' },
|
||||
{ label: '文件备份', value: 'file' },
|
||||
{ label: '完整备份', value: 'full' },
|
||||
];
|
||||
|
||||
export const statusOptions = [
|
||||
{ label: '成功', value: 'success' },
|
||||
{ label: '失败', value: 'failed' },
|
||||
{ label: '进行中', value: 'running' },
|
||||
];
|
||||
|
||||
export const statusColorMap = {
|
||||
success: 'success',
|
||||
failed: 'error',
|
||||
running: 'processing',
|
||||
};
|
||||
|
||||
export const gridOptions: VxeGridProps<BackupInfo> = {
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'name', title: '备份名称', minWidth: 150 },
|
||||
{ field: 'type', title: '备份类型', width: 120, formatter: ({ cellValue }) => {
|
||||
const option = typeOptions.find(item => item.value === cellValue);
|
||||
return option?.label || cellValue;
|
||||
}},
|
||||
{ field: 'size', title: '文件大小', width: 100 },
|
||||
{ field: 'status', title: '状态', width: 100, formatter: ({ cellValue }) => {
|
||||
const colorMap = {
|
||||
success: 'success',
|
||||
failed: 'error',
|
||||
running: 'processing',
|
||||
};
|
||||
const color = colorMap[cellValue] || 'default';
|
||||
const option = statusOptions.find(item => item.value === cellValue);
|
||||
return `<span class="ant-tag ant-tag-${color}">${option?.label || cellValue}</span>`;
|
||||
} },
|
||||
{ field: 'start_time', title: '开始时间', width: 180 },
|
||||
{ field: 'end_time', title: '结束时间', width: 180 },
|
||||
{ field: 'create_time', title: '创建时间', width: 180 },
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: BackupInfo) => {
|
||||
// This will be handled in the component
|
||||
},
|
||||
options: [
|
||||
{ code: 'download', text: '下载', icon: 'ant-design:download-outlined' },
|
||||
{ code: 'restore', text: '还原', icon: 'ant-design:reload-outlined' },
|
||||
{ code: 'delete', text: '删除', icon: 'ant-design:delete-outlined', danger: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
print: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
},
|
||||
};
|
||||
219
admin-vben/apps/web-antd/src/views/tools/backup/list.vue
Normal file
219
admin-vben/apps/web-antd/src/views/tools/backup/list.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<VbenVxeGrid
|
||||
ref="gridRef"
|
||||
:grid-options="gridOptions"
|
||||
:query-form-schema="queryFormSchema"
|
||||
@toolbar-button-click="handleToolbarButtonClick"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<VbenButton type="primary" @click="handleCreateBackup">
|
||||
<SvgIcon icon="mdi:backup" class="mr-1" />
|
||||
创建备份
|
||||
</VbenButton>
|
||||
</template>
|
||||
|
||||
<template #status="{ row }">
|
||||
<VbenTag :color="statusColorMap[row.status]">
|
||||
{{ getStatusLabel(row.status) }}
|
||||
</VbenTag>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<VbenButton
|
||||
size="small"
|
||||
type="primary"
|
||||
variant="text"
|
||||
@click="handleDownload(row)"
|
||||
>
|
||||
下载
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
size="small"
|
||||
type="primary"
|
||||
variant="text"
|
||||
@click="handleRestore(row)"
|
||||
>
|
||||
还原
|
||||
</VbenButton>
|
||||
<VbenPopconfirm
|
||||
title="确定删除该备份吗?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<VbenButton
|
||||
size="small"
|
||||
type="danger"
|
||||
variant="text"
|
||||
>
|
||||
{{ $t('common.delete') }}
|
||||
</VbenButton>
|
||||
</VbenPopconfirm>
|
||||
</template>
|
||||
</VbenVxeGrid>
|
||||
|
||||
<BackupFormModal
|
||||
v-model:visible="modalVisible"
|
||||
:id="editingId"
|
||||
@cancel="handleModalCancel"
|
||||
@submit="handleModalSubmit"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { BackupInfo, BackupForm } from './data';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useVbenVxeGrid, VbenButton, VbenPopconfirm, VbenTag, VbenVxeGrid } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { getBackupListApi, createBackupApi, deleteBackupApi, downloadBackupApi, restoreBackupApi } from '#/api/core/tools';
|
||||
import { SvgIcon } from '#/components/icon';
|
||||
|
||||
import BackupFormModal from './modules/form.vue';
|
||||
import { gridOptions, statusOptions, statusColorMap } from './data';
|
||||
|
||||
const gridRef = ref();
|
||||
const modalVisible = ref(false);
|
||||
const editingId = ref<number | undefined>();
|
||||
|
||||
const queryFormSchema = computed(() => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '备份名称',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'type',
|
||||
label: '备份类型',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '数据库备份', value: 'database' },
|
||||
{ label: '文件备份', value: 'file' },
|
||||
{ label: '完整备份', value: 'full' },
|
||||
],
|
||||
placeholder: '请选择备份类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
...statusOptions,
|
||||
],
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'DateRange',
|
||||
fieldName: 'create_time',
|
||||
label: '创建时间',
|
||||
componentProps: {
|
||||
placeholder: ['开始时间', '结束时间'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
queryFormSchema,
|
||||
});
|
||||
|
||||
function handleToolbarButtonClick(event: string) {
|
||||
switch (event) {
|
||||
case 'add':
|
||||
handleAdd();
|
||||
break;
|
||||
case 'refresh':
|
||||
handleRefresh();
|
||||
break;
|
||||
case 'export':
|
||||
handleExport();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
editingId.value = undefined;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function getStatusLabel(status: string): string {
|
||||
const option = statusOptions.find(item => item.value === status);
|
||||
return option?.label || status;
|
||||
}
|
||||
|
||||
async function handleDownload(row: BackupInfo) {
|
||||
try {
|
||||
await downloadBackupApi(row.id);
|
||||
$message.success('下载开始');
|
||||
} catch (error) {
|
||||
$message.error('下载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRestore(row: BackupInfo) {
|
||||
try {
|
||||
await restoreBackupApi(row.id);
|
||||
await handleRefresh();
|
||||
$message.success('还原成功');
|
||||
} catch (error) {
|
||||
$message.error('还原失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: BackupInfo) {
|
||||
try {
|
||||
await deleteBackupApi(row.id);
|
||||
await handleRefresh();
|
||||
$message.success('删除成功');
|
||||
} catch (error) {
|
||||
$message.error('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
function handleModalCancel() {
|
||||
modalVisible.value = false;
|
||||
editingId.value = undefined;
|
||||
}
|
||||
|
||||
async function handleModalSubmit(data: BackupForm) {
|
||||
try {
|
||||
if (editingId.value) {
|
||||
// Backup editing is not supported, only creation
|
||||
$message.info('备份不支持编辑');
|
||||
} else {
|
||||
await createBackupApi(data);
|
||||
$message.success('备份创建成功');
|
||||
}
|
||||
modalVisible.value = false;
|
||||
await handleRefresh();
|
||||
} catch (error) {
|
||||
$message.error(editingId.value ? '操作失败' : '创建失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRefresh() {
|
||||
await gridApi.query();
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
gridApi.exportData({
|
||||
filename: '备份列表',
|
||||
type: 'csv',
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateBackup() {
|
||||
handleAdd();
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div>
|
||||
<VbenForm
|
||||
:handle-submit="handleSubmit"
|
||||
:model="model"
|
||||
:schema="formSchemas"
|
||||
:show-default-actions="false"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<template #form-submit>
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<VbenButton @click="handleCancel" variant="outline">
|
||||
{{ $t('common.cancel') }}
|
||||
</VbenButton>
|
||||
<VbenButton type="primary" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { BackupForm } from '../data';
|
||||
|
||||
import { VbenButton, VbenForm, useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { useBackupFormSchemas } from './formSchemas';
|
||||
|
||||
interface Props {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', data: BackupForm): void;
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
id: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const [Drawer] = useVbenDrawer();
|
||||
const model = ref<BackupForm>({
|
||||
name: '',
|
||||
type: 'database',
|
||||
compress: 1,
|
||||
});
|
||||
|
||||
const formSchemas = useBackupFormSchemas();
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await Drawer?.formApi.validate();
|
||||
const formValues = Drawer?.formApi.getValues() || model.value;
|
||||
emit('submit', formValues);
|
||||
} catch (error) {
|
||||
console.error('Form validation failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
// Load backup data if editing
|
||||
onMounted(async () => {
|
||||
if (props.id) {
|
||||
try {
|
||||
// Load backup data
|
||||
const backupData = await getBackupDetailApi(props.id);
|
||||
model.value = { ...backupData };
|
||||
} catch (error) {
|
||||
console.error('Failed to load backup data:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { BackupForm } from '../data';
|
||||
|
||||
import { useVbenForm } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locale';
|
||||
|
||||
import { typeOptions } from '../data';
|
||||
|
||||
export const useBackupFormSchemas = () => {
|
||||
const formSchemas = computed(() => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '备份名称',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入备份名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'type',
|
||||
label: '备份类型',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: typeOptions,
|
||||
placeholder: '请选择备份类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'tables',
|
||||
label: '备份表',
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'database';
|
||||
}),
|
||||
componentProps: {
|
||||
mode: 'multiple',
|
||||
placeholder: '请选择要备份的数据表',
|
||||
options: [], // This should be populated with actual table list
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'exclude_tables',
|
||||
label: '排除表',
|
||||
ifShow: computed(() => {
|
||||
const form = useVbenForm().getValues();
|
||||
return form.type === 'database';
|
||||
}),
|
||||
componentProps: {
|
||||
mode: 'multiple',
|
||||
placeholder: '请选择要排除的数据表',
|
||||
options: [], // This should be populated with actual table list
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'compress',
|
||||
label: '压缩备份',
|
||||
defaultValue: 1,
|
||||
componentProps: {
|
||||
checkedChildren: '是',
|
||||
unCheckedChildren: '否',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return formSchemas;
|
||||
};
|
||||
Reference in New Issue
Block a user