chore: push latest changes

This commit is contained in:
wanwu
2025-11-16 22:13:57 +08:00
parent de821ae5fd
commit 7ede50739b
780 changed files with 101983 additions and 10460 deletions

View File

@@ -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}`);

View File

@@ -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": "存储配置"
}
}
}
}

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

View 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,
},
};

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

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

View File

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

View File

@@ -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' },
},
];
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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' },
},
];
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -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' },
},
];
};

View File

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

View File

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

View File

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

View File

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

View 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,
},
};

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

View 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>订单号:</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>

View File

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

View 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,
},
};

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

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

View File

@@ -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 },
],
},
},
},
];

View File

@@ -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 },
],
},
},
},
];

View File

@@ -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 },
],
},
},
},
];

View File

@@ -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 },
],
},
},
},
];

View 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,
},
};

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

View File

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

View File

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

View File

@@ -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 },
],
},
},
},
];

View File

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

View 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,
},
};

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

View File

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

View File

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

View File

@@ -263,6 +263,7 @@ const notificationEl : any = null
const elNotificationClick = () => {
showDialog.value = true
cloudBuildModalApi.open()
active.value = 'build'
getCloudBuildLogFn()
}
@@ -284,18 +285,22 @@ const open = async () => {
loading.value = false
cloudBuildTask.value = data
showDialog.value = true
cloudBuildModalApi.open()
getCloudBuildLogFn()
}).catch(() => {
showDialog.value = false
cloudBuildModalApi.close()
loading.value = false
})
} else {
loading.value = false
cloudBuildCheck.value = data
showDialog.value = true
cloudBuildModalApi.open()
}
}).catch(() => {
showDialog.value = false
cloudBuildModalApi.close()
})
}

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="dialogVisible" :title="t('gxx')" width="850" :destroy-on-close="true">
<ModalUpgradeLog :class="'w-[850px] time-dialog'" :title="t('gxx')">
<el-card class="box-card !border-none" shadow="never" >
<div v-loading="loading">
<div class="text-page-title mb-[20px]">历史版本</div>
@@ -28,12 +28,13 @@
</div>
</div>
</el-card>
</el-dialog>
</ModalUpgradeLog>
</template>
<script lang="ts" setup>
import { computed, ref, defineProps, nextTick } from "vue"
import { t } from "@/lang"
import { getAppVersionList, getFrameworkVersionList } from "@/app/api/module"
import { useVbenModal } from '@vben/common-ui'
const props = defineProps({
upgradeKey: {
@@ -65,6 +66,7 @@ const getAppVersionListFn = () => {
return
}else{
dialogVisible.value = true
upgradeLogModalApi.open()
}
})
}
@@ -80,6 +82,7 @@ const getFrameworkVersionListFn = () => {
})
frameworkVersionList.value = data
dialogVisible.value = true
upgradeLogModalApi.open()
})
}
@@ -88,6 +91,7 @@ const activeName = ref(0)
// 提交信息
const loading = ref(true)
const dialogVisible = ref(false)
const [ModalUpgradeLog, upgradeLogModalApi] = useVbenModal()
const open = async () => {
nextTick(() => {
activeName.value = 0 // 重置

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="t('upgrade.title')" width="850px" :close-on-click-modal="false" :close-on-press-escape="false" :before-close="dialogClose">
<ModalUpgrade :class="'w-[850px]'" :title="t('upgrade.title')" :close-on-click-modal="false" :close-on-press-escape="false" @close="dialogClose">
<template v-if="upgradeContent">
<!-- 检测服务是否到期 -->
@@ -203,9 +203,9 @@
</template>
</div>
</template>
</el-dialog>
</ModalUpgrade>
<el-dialog v-model="upgradeTipsShowDialog" :title="t('warning')" width="500px" draggable>
<ModalUpgradeTips :class="'w-[500px]'" :title="t('warning')">
<span v-html="t('upgrade.upgradeTips')"></span>
<template #footer>
<div class="flex justify-end">
@@ -214,9 +214,9 @@
<el-button @click="upgradeTipsShowDialog = false">{{ t("cancel") }}</el-button>
</div>
</template>
</el-dialog>
</ModalUpgradeTips>
<el-dialog v-model="cloudBuildErrorTipsShowDialog" :title="t('warning')" width="500px" draggable :show-close="false">
<ModalCloudBuildError :class="'w-[500px]'" :title="t('warning')">
<span v-html="t('upgrade.cloudBuildErrorTips')"></span>
<template #footer>
<div class="flex justify-end">
@@ -225,7 +225,7 @@
<el-button @click="cloudBuildError('rollback')" type="primary">{{ t("upgrade.rollback") }}</el-button>
</div>
</template>
</el-dialog>
</ModalCloudBuildError>
</template>
<script lang="ts" setup>
@@ -544,6 +544,7 @@ const open = (addonKey: string = '', callback = null) => {
handleUpgrade()
} else {
upgradeTipsShowDialog.value = true
upgradeTipsModalApi.open()
}
if (callback) callback()
}).catch(() => {
@@ -631,6 +632,7 @@ const clearUpgradeTaskFn = () => {
*/
const cloudBuildError = (event: string) => {
cloudBuildErrorTipsShowDialog.value = false
cloudErrorModalApi.close()
switch (event) {
case 'local':
upgradeUserOperate(event).then(() => {
@@ -658,7 +660,11 @@ const timeSplit = (str: string) => {
const upgradeTipsConfirm = (isLock: boolean = false) => {
isLock && Storage.set({ key: 'upgradeTipsLock', data: isLock })
upgradeTipsShowDialog.value = false
!isLock && (showDialog.value = true)
upgradeTipsModalApi.close()
if (!isLock) {
showDialog.value = true
upgradeModalApi.open()
}
}
const activeName = ref(0)
const numberOfSteps = ref(0)

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="dialogVisible" :title="t('accountSettings')" width="500">
<ModalUserInfoEdit :class="'w-[500px]'" :title="t('accountSettings')">
<el-form :model="saveInfo" label-width="90px" ref="formRef" class="page-form">
<el-form-item :label="t('headImg')">
<upload-image v-model="saveInfo.head_img" :limit="1" imageFit="cover" />
@@ -14,10 +14,10 @@
<template #footer>
<div class="flex justify-end">
<el-button type="primary" @click="submitForm(formRef)" :loading="loading">{{ t('save') }}</el-button>
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
</div>
</template>
</el-dialog>
</ModalUserInfoEdit>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
@@ -26,6 +26,7 @@ import type { FormInstance } from 'element-plus'
import { deepClone } from '@/utils/common'
import { getUserInfo, setUserInfo } from '@/app/api/personal'
import useUserStore from '@/stores/modules/user'
import { useVbenModal } from '@vben/common-ui'
const userStore = useUserStore()
// 提交信息
@@ -33,6 +34,7 @@ const saveInfo: any = reactive({})
const formRef = ref<FormInstance>()
const loading = ref(true)
const dialogVisible = ref(false)
const [ModalUserInfoEdit, userInfoModalApi] = useVbenModal()
/**
* 获取用户信息
*/
@@ -48,6 +50,7 @@ const getUserInfoFn = () => {
getUserInfoFn()
const open = ()=>{
dialogVisible.value = true
userInfoModalApi.open()
getUserInfoFn()
}
const submitForm = (formEl: FormInstance | undefined) => {
@@ -58,6 +61,7 @@ const submitForm = (formEl: FormInstance | undefined) => {
setUserInfo(saveInfo).then((res: any) => {
loading.value = false
dialogVisible.value = false
userInfoModalApi.close()
let data: any = deepClone(userStore.userInfo)
data.head_img = saveInfo.head_img
userStore.setUserInfo(data)
@@ -69,6 +73,7 @@ const submitForm = (formEl: FormInstance | undefined) => {
}
})
}
const cancel = () => { dialogVisible.value = false; userInfoModalApi.close() }
defineExpose({
open
})

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="popTitle" width="500px" :destroy-on-close="true">
<ModalRole :class="'w-[500px]'" :title="popTitle">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('roleName')" prop="role_name">
<el-input v-model.trim="formData.role_name" :placeholder="t('roleNamePlaceholder')" clearable :disabled="formData.uid" class="input-width" maxlength="10" :show-word-limit="true" />
@@ -29,11 +29,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</ModalRole>
</template>
<script lang="ts" setup async>
@@ -43,7 +43,7 @@ import type { FormInstance } from 'element-plus'
import { addRole, editRole, getRoleInfo, getSiteMenus } from '@/app/api/sys'
import { debounce } from '@/utils/common'
const showDialog = ref(false)
const [ModalRole, modalApi] = useVbenModal()
const loading = ref(false)
const isOpen = ref(true)
@@ -148,7 +148,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
save(data).then(res => {
loading.value = false
showDialog.value = false
modalApi.close()
emit('complete')
}).catch(() => {
@@ -184,6 +184,7 @@ const setFormData = async (row: any = null) => {
})
}
loading.value = false
modalApi.open()
}
function checked (menuKey:string, data:any, newArr:any) {
@@ -201,10 +202,9 @@ function checked (menuKey:string, data:any, newArr:any) {
})
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="popTitle" width="500px" :destroy-on-close="true">
<ModalUser :class="'w-[500px]'" :title="popTitle">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<!-- <el-form-item :label="t('accountNumber')" v-if="!formData.uid" prop="uid">
@@ -53,11 +53,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalUser>
</template>
<script lang="ts" setup>
@@ -95,7 +95,7 @@ const needAddUserInfo = computed(() => {
}
})
const showDialog = ref(false)
const [ModalUser, modalApi] = useVbenModal()
const loading = ref(false)
let popTitle: string = ''
@@ -181,7 +181,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
if (!formData.uid && typeof uid.value == 'number') data.uid = uid.value
save(data).then(res => {
loading.value = false
showDialog.value = false
modalApi.close()
!formData.uid && getUserList()
emit('complete')
}).catch(() => {
@@ -206,12 +206,12 @@ const setFormData = async (row: any = null) => {
})
}
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="formData.id ? t('updateAppVersion') : t('addAppVersion')" width="60%" class="diy-dialog-wrap" :destroy-on-close="true">
<ModalAppVersionEdit :class="'w-[60%] diy-dialog-wrap'" :title="formData.id ? t('updateAppVersion') : t('addAppVersion')">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
@@ -102,7 +102,7 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<view v-show="step == 1">
<el-button type="primary" class="ml-3" @click="step = 2">{{
t('next')
@@ -116,7 +116,7 @@
</view>
</span>
</template>
</el-dialog>
</ModalAppVersionEdit>
<generate-sing-cert ref="generateSingCertRef"/>
</template>
@@ -127,8 +127,9 @@ import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { addVersion, editVersion, getVersionInfo, getAppPlatform } from '@/app/api/app'
import GenerateSingCert from '@/app/views/channel/app/components/generate-sing-cert.vue'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [ModalAppVersionEdit, versionModalApi] = useVbenModal()
const loading = ref(false)
const appPlatform = ref({})
const step = ref(1)
@@ -242,7 +243,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
save(formData).then(res => {
loading.value = false
showDialog.value = false
versionModalApi.close()
emit('complete')
}).catch(() => {
loading.value = false
@@ -263,7 +264,7 @@ const setFormData = async (row: any = null) => {
loading.value = false
}
watch(() => showDialog.value, () => {
watch(() => versionModalApi.getVisible?.(), () => {
step.value = 1
})
@@ -271,10 +272,8 @@ const windowOpen = (url: string) => {
window.open(url)
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { versionModalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" title="生成Android证书" width="50%" class="diy-dialog-wrap" :destroy-on-close="true">
<ModalGenerateCert :class="'w-[50%] diy-dialog-wrap'" title="生成Android证书">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item label="证书别名" prop="key_alias">
@@ -62,11 +62,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button @click="cancel">取消</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">生成</el-button>
</span>
</template>
</el-dialog>
</ModalGenerateCert>
</template>
<script setup lang="ts">
@@ -74,8 +74,9 @@ import { ref, reactive, computed } from 'vue'
import type { FormInstance } from 'element-plus'
import { generateSingCert } from '@/app/api/app'
import { img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [ModalGenerateCert, certModalApi] = useVbenModal()
const moreInfo = ref(false)
const loading = ref(false)
@@ -137,7 +138,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
generateSingCert(formData).then(res => {
loading.value = false
showDialog.value = false
certModalApi.close()
window.open(img(res.data), '_blank')
})
}
@@ -145,12 +146,11 @@ const confirm = async (formEl: FormInstance | undefined) => {
const open = async (row: any = null) => {
Object.assign(formData, initialFormData)
showDialog.value = true
certModalApi.open()
}
defineExpose({
open
})
const cancel = () => { certModalApi.close() }
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@@ -54,7 +54,7 @@
</div>
</el-card>
<el-dialog v-model="dialogVisible" :title="t('codeDownTwoDesc')" width="30%" :before-close="handleClose">
<ModalWeappRelease :class="'w-[30%]'" :title="t('codeDownTwoDesc')" @close="handleClose">
<el-form ref="ruleFormRef" :model="form" label-width="120px">
<el-form-item prop="code" :label="t('code')">
<el-input v-model.trim="form.code" :placeholder="t('codePlaceholder')" onkeyup="this.value = this.value.replace(/[^\d\.]/g,'');" />
@@ -68,29 +68,29 @@
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button>
<el-button @click="releaseCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="insert">
{{ t('confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</ModalWeappRelease>
<el-dialog v-model="failReasonDialogVisible" :title="t('failReason')" width="60%">
<ModalFailReason :class="'w-[60%]'" :title="t('failReason')">
<el-scrollbar class="h-[60vh] w-full whitespace-pre-wrap p-[20px]">
<div v-html="failReason"></div>
</el-scrollbar>
</el-dialog>
</ModalFailReason>
<el-dialog v-model="uploadSuccessShowDialog" :title="t('warning')" width="500px" draggable>
<ModalUploadSuccess :class="'w-[500px]'" :title="t('warning')">
<span v-html="t('uploadSuccessTips')"></span>
<template #footer>
<div class="flex justify-end">
<el-button @click="knownToKnow" type="primary">{{ t('knownToKnow') }}</el-button>
<el-button @click="uploadSuccessShowDialog = false" type="primary" plain>{{ t('confirm') }}</el-button>
<el-button @click="uploadSuccessCancel" type="primary" plain>{{ t('confirm') }}</el-button>
</div>
</template>
</el-dialog>
</ModalUploadSuccess>
</div>
</template>
@@ -105,11 +105,13 @@ import { ElMessageBox } from 'element-plus'
import { AnyObject } from '@/types/global'
import Storage from '@/utils/storage'
import { siteWeappCommit, undoAudit } from "@/app/api/wxoplatform";
import { useVbenModal } from '@vben/common-ui'
const route = useRoute()
const router = useRouter()
const pageName = route.meta.title
const dialogVisible = ref(false)
const [ModalWeappRelease, releaseModalApi] = useVbenModal()
const loading = ref(true)
const weappTableData:{
page: number,
@@ -131,7 +133,9 @@ const form = ref({
content: ''
})
const uploadSuccessShowDialog = ref(false)
const [ModalUploadSuccess, uploadSuccessModalApi] = useVbenModal()
const authCode = ref('')
const [ModalFailReason, failReasonModalApi] = useVbenModal()
getAuthInfo().then(res => {
if (res.data.data && res.data.data.auth_code) {
@@ -187,6 +191,7 @@ getWeappVersionListFn()
const handleClose = () => {
ruleFormRef.value.clearValidate()
}
const releaseCancel = () => { releaseModalApi.close() }
const uploading = ref(false)
const insert = () => {
@@ -239,7 +244,7 @@ const getWeappUploadLogFn = (key: string) => {
if (last.code == 1 && last.percent == 100) {
getWeappVersionListFn()
getWeappPreviewImage()
!Storage.get('weappUploadTipsLock') && (uploadSuccessShowDialog.value = true)
!Storage.get('weappUploadTipsLock') && uploadSuccessModalApi.open()
return
}
setTimeout(() => {
@@ -308,12 +313,15 @@ const failReasonDialogVisible = ref(false)
const handleFailReason = (data: any) => {
failReason.value = data.fail_reason
failReasonDialogVisible.value = true
failReasonModalApi.open()
}
const knownToKnow = () => {
Storage.set({ key: 'weappUploadTipsLock', data: true })
uploadSuccessShowDialog.value = false
uploadSuccessModalApi.close()
}
const uploadSuccessCancel = () => { uploadSuccessModalApi.close() }
const againUpload = () => {
if (uploading.value) return

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="t('functionSetting')" width="700px" :destroy-on-close="true">
<ModalWeappDomain :class="'w-[700px]'" :title="t('functionSetting')">
<el-form :model="formData" label-width="180px" ref="formRef" :rules="formRules" class="page-form pr-[100px]" v-loading="loading">
<el-form-item :label="t('requestUrl')" prop="requestdomain">
<el-input v-model="formData.requestdomain" :placeholder="t('requestdomainPlaceholder')" type="textarea">
@@ -29,11 +29,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</ModalWeappDomain>
</template>
<script lang="ts" setup>
@@ -42,8 +42,9 @@ import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { setWeappDomain } from '@/app/api/weapp'
import Test from '@/utils/test'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [ModalWeappDomain, domainModalApi] = useVbenModal()
const loading = ref(false)
/**
@@ -135,7 +136,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
setWeappDomain(data).then(res => {
loading.value = false
showDialog.value = false
domainModalApi.close()
emit('complete', data)
}).catch(() => {
loading.value = false
@@ -154,10 +155,8 @@ const setFormData = async (data: any = null) => {
}
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { domainModalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="t('privacyAgreementTitle')" width="900px" :destroy-on-close="true">
<ModalWeappPrivacy :class="'w-[900px]'" :title="t('privacyAgreementTitle')">
<div class="h-[60vh]">
<el-scrollbar>
<el-form :model="formData" label-width="auto" label-position="left" ref="formRef" :rules="formRules"
@@ -95,11 +95,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalWeappPrivacy>
</template>
<script lang="ts" setup>
@@ -108,8 +108,9 @@ import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import SettingList from './setting-list.vue'
import { getWeappPrivacySetting, setWeappPrivacySetting } from '@/app/api/weapp'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [ModalWeappPrivacy, privacyModalApi] = useVbenModal()
const loading = ref(false)
const settingListRef = ref(null)
const sdkSettingListRef = ref(null)
@@ -187,7 +188,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
setWeappPrivacySetting(data).then(res => {
loading.value = false
showDialog.value = false
privacyModalApi.close()
emit('complete', data)
}).catch(() => {
loading.value = false
@@ -209,14 +210,12 @@ const setFormData = async () => {
formData.store_expire_timestamp = data.owner_setting.store_expire_timestamp
}
}
showDialog.value = true
privacyModalApi.open()
})
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { privacyModalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -114,7 +114,7 @@
</div>
</div>
<el-dialog v-model="settingTypeDialog" :title="t('settingTypeTitle')" width="500px" :destroy-on-close="true">
<ModalSettingType :class="'w-[500px]'" :title="t('settingTypeTitle')">
<el-checkbox-group v-model="checkList">
<template v-for="(item, index) in privacyList">
<el-checkbox :label="item.privacy_key" v-if="!checkIsSelected(item.privacy_key)">{{ item.privacy_text }}</el-checkbox>
@@ -122,16 +122,17 @@
</el-checkbox-group>
<template #footer>
<span class="dialog-footer">
<el-button @click="settingTypeDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="selectSettingType()">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalSettingType>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { t } from '@/lang'
import { useVbenModal } from '@vben/common-ui'
const props = defineProps({
modelValue: {
@@ -142,6 +143,7 @@ const props = defineProps({
}
})
const [ModalSettingType, settingModalApi] = useVbenModal()
const settingTypeDialog = ref(false)
const privacyList = ref([
@@ -199,11 +201,13 @@ const selectSettingType = () => {
})
})
settingTypeDialog.value = false
settingModalApi.close()
checkList.value = []
}
const addSettingList = () => {
settingTypeDialog.value = true
settingModalApi.open()
}
const selectedSettingType = computed(() => {
@@ -219,3 +223,4 @@ defineExpose({
</script>
<style lang="scss" scoped></style>
const cancel = () => { settingTypeDialog.value = false; settingModalApi.close() }

View File

@@ -2,7 +2,7 @@
<div @click="openDialog">
<slot></slot>
</div>
<el-dialog v-model="showDialog" :title="t('upload.select' + type)" width="60%" class="attachment-dialog" :destroy-on-close="true">
<ModalWechatMedia :class="'w-[60%] attachment-dialog'" :title="t('upload.select' + type)">
<div class="flex border-t border-b main-wrap border-color w-full h-[40vh]">
<!-- 素材 -->
<div class="attachment-list-wrap flex flex-col p-[15px] flex-1 overflow-hidden">
@@ -88,11 +88,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="confirm">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalWechatMedia>
</template>
<script lang="ts" setup>
@@ -101,6 +101,7 @@ import { t } from '@/lang'
import UploadMedia from './upload-media.vue'
import { img, debounce } from '@/utils/common'
import { getMediaList, syncNews } from '@/app/api/wechat'
import { useVbenModal } from '@vben/common-ui'
const prop = defineProps({
type: {
@@ -109,11 +110,11 @@ const prop = defineProps({
}
})
const showDialog = ref(false)
const [ModalWechatMedia, mediaModalApi] = useVbenModal()
const openDialog = () => {
prop.type == 'news' && waterfall()
showDialog.value = true
mediaModalApi.open()
}
const attachment: Record<string, any> = reactive({
@@ -150,7 +151,9 @@ const selectedFile: Record<string, any> = ref({})
const confirm = () => {
emits('success', selectedFile.value)
mediaModalApi.close()
}
const cancel = () => { mediaModalApi.close() }
const syncLoading = ref(false)
const syncWechatNews = () => {

View File

@@ -66,16 +66,16 @@
</div>
</div>
<el-dialog v-model="showDialog" :title="t('addReplyContent')" width="60%" :destroy-on-close="true">
<ModalReplyContent :class="'w-[60%]'" :title="t('addReplyContent')">
<reply-form v-model="replyContent" ref="ReplyRef"/>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancelReply">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="addReplyContent">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalReplyContent>
</div>
</template>
@@ -88,6 +88,7 @@ import { ArrowLeft } from '@element-plus/icons-vue'
import { useRoute, useRouter } from 'vue-router'
import ReplyForm from '@/app/views/channel/wechat/components/reply-form.vue'
import NewsCard from '@/app/views/channel/wechat/components/news-card.vue'
import { useVbenModal } from '@vben/common-ui'
const route = useRoute()
const router = useRouter()
@@ -96,6 +97,7 @@ const back = () => {
router.push('/channel/wechat/reply')
}
const [ModalReplyContent, replyModalApi] = useVbenModal()
const showDialog = ref(false)
const formData: any = reactive({
@@ -116,9 +118,11 @@ const addReplyContent = () => {
formData.content.push(replyContent.value)
replyContent.value = {}
showDialog.value = false
replyModalApi.close()
}
})
}
const cancelReply = () => { showDialog.value = false; replyModalApi.close() }
const removeContent = (index: number) => {
formData.content.splice(index, 1)

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="t('dictData')" width="60%" class="diy-dialog-wrap" :destroy-on-close="true">
<ModalDict :class="'w-[60%] diy-dialog-wrap'" :title="t('dictData')">
<div class="mb-[10px]">
<el-button type="primary" @click="addEvent">
{{ t('addDictData') }}
@@ -20,11 +20,11 @@
</el-table>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="confirm()">{{ t('confirm') }}</el-button>
</span>
</template>
<el-dialog v-model="dialogVisible" :title="type != 'edit' ? t('addDictData') : t('editDictData')" width="480" class="diy-dialog-wrap" :destroy-on-close="true">
<ModalDictItem :class="'w-[480px] diy-dialog-wrap'" :title="type != 'edit' ? t('addDictData') : t('editDictData')">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form">
<el-form-item :label="t('name')">
<el-input v-model.trim="name" disabled class="input-width" />
@@ -47,12 +47,12 @@
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button>
<el-button @click="itemCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="submit(formRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</el-dialog>
</ModalDictItem>
</ModalDict>
</template>
<script lang="ts" setup>
@@ -61,10 +61,11 @@ import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { setDictData, getDictInfo } from '@/app/api/dict'
import { cloneDeep } from 'lodash-es'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [ModalDict, dictModalApi] = useVbenModal()
const loading = ref(false)
const dialogVisible = ref(false)
const [ModalDictItem, itemModalApi] = useVbenModal()
const tableDate = ref<Array<any>>([])
const id = ref()
@@ -95,7 +96,7 @@ const formRules = computed(() => {
const addEvent = () => {
type.value = 'add'
formData.value = cloneDeep(initialFormData)
dialogVisible.value = true
itemModalApi.open()
}
const tableIndex = ref(0)
const editEvent = (row: any, index: number) => {
@@ -103,7 +104,7 @@ const editEvent = (row: any, index: number) => {
tableIndex.value = index
formData.value = cloneDeep(initialFormData)
formData.value = Object.assign(formData.value, cloneDeep(row))
dialogVisible.value = true
itemModalApi.open()
}
/**
* 表单确认
@@ -118,7 +119,7 @@ const submit = async (formEl: FormInstance | undefined) => {
tableDate.value.splice(tableIndex.value, 1, cloneDeep(formData.value))
}
tableDate.value.sort(function (a, b) { return b.sort - a.sort })
dialogVisible.value = false
itemModalApi.close()
}
})
}
@@ -137,7 +138,7 @@ const confirm = async () => {
loading.value = true
setDictData(id.value, { dictionary: JSON.stringify(tableDate.value) }).then(res => {
loading.value = false
showDialog.value = false
dictModalApi.close()
emit('complete')
}).catch(() => {
loading.value = false
@@ -145,7 +146,7 @@ const confirm = async () => {
}
const setFormData = async (row: any = null) => {
showDialog.value = true
dictModalApi.open()
loading.value = true
id.value = row.id
name.value = row.name
@@ -154,10 +155,9 @@ const setFormData = async (row: any = null) => {
loading.value = false
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { dictModalApi.close() }
const itemCancel = () => { itemModalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,34 +1,17 @@
<template>
<el-dialog v-model="showDialog" :title="formData.id ? t('updateDict') : t('addDict')" width="480" class="diy-dialog-wrap" :destroy-on-close="true">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('name')" prop="name">
<el-input v-model.trim="formData.name" clearable maxlength="40" show-word-limit :placeholder="t('namePlaceholder')" class="input-width" />
</el-form-item>
<el-form-item :label="t('key')" prop="key">
<el-input v-model.trim="formData.key" clearable maxlength="40" show-word-limit :placeholder="t('keyPlaceholder')" class="input-width" />
<p class="form-tip">{{ t('keyFormatTips') }}</p>
</el-form-item>
<el-form-item :label="t('memo')">
<el-input v-model.trim="formData.memo" type="textarea" clearable :placeholder="t('memoPlaceholder')" class="input-width" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
<Modal :class="'w-[480px]'" :title="formModel.id ? t('updateDict') : t('addDict')">
<BaseForm />
<p class="form-tip">{{ t('keyFormatTips') }}</p>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { addDict, editDict, getDictInfo } from '@/app/api/dict'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const loading = ref(false)
/**
@@ -40,80 +23,41 @@ const initialFormData = {
key: '',
memo: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
name: [
{ required: true, message: t('namePlaceholder'), trigger: 'blur' }
],
key: [
{ required: true, message: t('keyPlaceholder'), trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
if (/^[a-zA-Z_]+$/.test(value)) {
callback()
} else {
callback(new Error(t('keyFormatTips')))
}
},
trigger: 'blur'
}
],
data: [
{ required: true, message: t('dataPlaceholder'), trigger: 'blur' }
]
}
const formModel: Record<string, any> = reactive({ ...initialFormData })
const [Modal, modalApi] = useVbenModal()
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
loading.value = true
const save = values.id ? editDict : addDict
save(values).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
},
layout: 'horizontal',
schema: [
{ component: 'Input', fieldName: 'name', label: t('name'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'key', label: t('key'), rules: [ { required: true }, { validator: (v) => (/^[a-zA-Z_]+$/.test(v) ? true : t('keyFormatTips')) } ] },
{ component: 'Textarea', fieldName: 'memo', label: t('memo') }
],
wrapperClass: 'grid-cols-1'
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
const save = formData.id ? editDict : addDict
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
save(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
})
}
})
}
const confirm = async () => { if (loading.value) return; formApi.submit() }
const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData)
loading.value = true
if (row) {
const data = await (await getDictInfo(row.id)).data
if (data) {
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
}
}
loading.value = false
Object.assign(formModel, initialFormData)
loading.value = true
if (row) {
const data = await (await getDictInfo(row.id)).data
if (data) { Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] }) }
}
formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -89,7 +89,7 @@
</el-form>
</div>
<el-dialog v-model="showDialog" :title="t('selectStyle')" width="800px">
<ModalStyleSelect :class="'w-[800px]'" :title="t('selectStyle')">
<div class="flex flex-wrap">
<div class="flex items-center justify-center overflow-hidden w-[32%] h-[100px] mr-[2%] mb-[15px] cursor-pointer border bg-gray-50"
@@ -112,12 +112,12 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="changeStyle">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalStyleSelect>
</div>
<!-- 样式 -->
@@ -178,6 +178,7 @@ import { t } from '@/lang'
import { watch, ref } from 'vue'
import useDiyStore from '@/stores/modules/diy'
import { img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
const diyStore = useDiyStore()
@@ -212,9 +213,9 @@ watch(
}, { deep: true }
)
const showDialog = ref(false)
const [ModalStyleSelect, styleModalApi] = useVbenModal()
const showStyle = () => {
showDialog.value = true
styleModalApi.open()
}
const selectStyle = ref('style-1')
@@ -234,8 +235,9 @@ const changeStyle = () => {
break
}
diyStore.global.topStatusBar.style = selectStyle.value
showDialog.value = false
styleModalApi.close()
}
const cancel = () => { styleModalApi.close() }
const selectImg = (url: any) => {
const image = new Image()

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="dialogThemeVisible" title="编辑色调" width="850px" align-center destroy-on-close="true">
<ModalEditTheme :class="'w-[850px]'" title="编辑色调" align-center>
<el-form :model="openData" label-width="150px" :rules="formRules">
<el-form-item label="色调名称" prop="title">
<el-input v-model="openData.title" placeholder="请输入色调名称" maxlength="15" class="!w-[250px]" :disabled="openData.id != ''" @keydown.enter.native.prevent />
@@ -34,12 +34,12 @@
<add-theme-component ref="addThemeRef" @confirm="addThemeConfirm" />
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogThemeVisible = false">取消</el-button>
<el-button @click="cancel">取消</el-button>
<el-button type="primary" plain @click="resetConfirmFn()">重置</el-button>
<el-button type="primary" @click="confirmFn(formRef)">保存</el-button>
</div>
</template>
</el-dialog>
</ModalEditTheme>
</template>
<script setup lang="ts">
@@ -51,10 +51,11 @@ import addThemeComponent from './add-theme.vue'
import useDiyStore from '@/stores/modules/diy'
import { addTheme, editTheme } from '@/app/api/diy'
import type { FormInstance } from 'element-plus'
import { useVbenModal } from '@vben/common-ui'
const diyStore = useDiyStore()
const dialogThemeVisible = ref(false)
const [ModalEditTheme, themeModalApi] = useVbenModal()
const addThemeRef = ref()
const openData: Record<string, any> = reactive({ // 用于接收弹窗打开时的参数
title: '',
@@ -93,7 +94,7 @@ const open = (res: any) => { // 参数: title=>色调名称key=>区分系
formData.value.forEach((item, index) => {
item.value = res.theme[item.label] ? res.theme[item.label] : item.value
})
dialogThemeVisible.value = true
themeModalApi.open()
}
// 新增颜色
@@ -218,7 +219,7 @@ const confirmFn = async (formEl: FormInstance | undefined) => {
api(params).then((res: any) => {
confirmRepeat = false
dialogThemeVisible.value = false
themeModalApi.close()
emit('confirm', params)
}).catch(() => {
confirmRepeat = false
@@ -226,6 +227,7 @@ const confirmFn = async (formEl: FormInstance | undefined) => {
}
})
}
const cancel = () => { themeModalApi.close() }
const applyOpacity = (color, opacity) => {
// 解析十六进制或 RGBA 格式
@@ -258,10 +260,7 @@ const colorPickerChange = (e: any, data: any) => {
})
}
}
defineExpose({
dialogThemeVisible,
open
})
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-dialog v-model="showDialog" :title="t('submitSuccess')" width="850px" :close-on-press-escape="true" :destroy-on-close="true" :close-on-click-modal="false">
<ModalFormSubmit :class="'w-[850px]'" :title="t('submitSuccess')" :close-on-click-modal="false">
<div class="flex flex-1 mt-[24px] mx-[24px] mb-0">
<div class="preview-wrap">
@@ -209,7 +209,7 @@
<template #footer>
<div class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="confirm">{{ t('save') }}</el-button>
</div>
</template>
@@ -227,7 +227,9 @@ import storage from '@/utils/storage'
import { filterNumber } from '@/utils/common'
import { getUrl } from '@/app/api/sys'
import { getFormSubmitConfig,editDiyFormSubmitConfig } from '@/app/api/diy_form'
import { useVbenModal } from '@vben/common-ui'
const [ModalFormSubmit, formSubmitModalApi] = useVbenModal()
const showDialog = ref(false)
const repeat = ref(false)
@@ -350,12 +352,15 @@ const confirm = () => {
editDiyFormSubmitConfig(data).then(res => {
repeat.value = false
showDialog.value = false
formSubmitModalApi.close()
emit('complete')
}).catch(err => {
repeat.value = false
})
}
const cancel = () => { showDialog.value = false; formSubmitModalApi.close() }
defineExpose({
showDialog,
setFormData

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="t('writeSet')" width="600px" class="diy-dialog-wrap" :close-on-press-escape="true" :destroy-on-close="true" :close-on-click-modal="false">
<ModalFormWrite :class="'w-[600px] diy-dialog-wrap'" :title="t('writeSet')" :close-on-click-modal="false">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
@@ -108,11 +108,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalFormWrite>
</template>
<script lang="ts" setup>
@@ -122,7 +122,9 @@ import type { FormInstance } from 'element-plus'
import { filterNumber } from '@/utils/common'
import {getMemberLabelAll,getMemberLevelAll } from '@/app/api/member'
import { getFormWriteConfig,editDiyFormWriteConfig } from '@/app/api/diy_form'
import { useVbenModal } from '@vben/common-ui'
const [ModalFormWrite, formWriteModalApi] = useVbenModal()
const showDialog = ref(false)
const loading = ref(false)
@@ -308,6 +310,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
editDiyFormWriteConfig(data).then(res => {
loading.value = false
showDialog.value = false
formWriteModalApi.close()
emit('complete')
}).catch(err => {
loading.value = false
@@ -325,6 +328,7 @@ defineExpose({
showDialog,
setFormData
})
const cancel = () => { showDialog.value = false; formWriteModalApi.close() }
</script>
<style lang="scss" scoped></style>

View File

@@ -94,7 +94,7 @@
</el-card>
<!--添加表单-->
<el-dialog v-model="dialogVisible" :title="t('addFormTips')" width="980px">
<ModalAddFormTips :class="'w-[980px]'" :title="t('addFormTips')">
<el-form :model="formData" ref="formRef" :rules="formRules">
<!-- <el-form-item :label="t('title')" prop="title">-->
<!-- <el-input v-model.trim="formData.title" :placeholder="t('titlePlaceholder')" clearable maxlength="12" show-word-limit class="w-full" />-->
@@ -120,14 +120,14 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button>
<el-button @click="cancelAddForm">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="addEvent(formRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalAddFormTips>
<!-- 分享设置-->
<el-dialog v-model="shareDialogVisible" :title="t('shareSet')" width="30%">
<ModalShareSet :class="'w-[30%]'" :title="t('shareSet')">
<el-tabs v-model="tabShareType">
<el-tab-pane :label="t('wechat')" name="wechat"></el-tab-pane>
<el-tab-pane :label="t('weapp')" name="weapp"></el-tab-pane>
@@ -149,11 +149,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="shareDialogVisible = false">{{ t('cancel') }}</el-button>
<el-button @click="cancelShare">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="shareEvent(shareFormRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalShareSet>
<!-- 推广弹出框 -->
<spread-popup ref="spreadPopupRef" />
@@ -179,6 +179,7 @@ import { getFormType, getApps, getDiyFormPageList, deleteDiyForm, editDiyFormSha
import { FormInstance, ElMessage, ElMessageBox } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { setTablePageStorage, getTablePageStorage, img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
import recordsDetail from '@/app/views/diy_form/records.vue'
import formSubmitPopup from '@/app/views/diy_form/components/form-submit-popup.vue'
@@ -219,6 +220,7 @@ const formRules = computed(() => {
})
const formRef = ref<FormInstance>()
const [ModalAddFormTips, addFormModalApi] = useVbenModal()
const dialogVisible = ref(false)
const addEvent = async (formEl: FormInstance | undefined) => {
if (!formEl) return
@@ -232,11 +234,13 @@ const addEvent = async (formEl: FormInstance | undefined) => {
})
window.open(url.href)
dialogVisible.value = false
addFormModalApi.close()
formData.title = ''
formData.type = ''
}
})
}
const cancelAddForm = () => { dialogVisible.value = false; addFormModalApi.close() }
const showClick = (row: any) => {
const data = row.status === 1 ? 0 : 1
@@ -472,6 +476,7 @@ const shareFormData = reactive({
}
})
const [ModalShareSet, shareModalApi] = useVbenModal()
const shareDialogVisible = ref(false)
const shareFormRules = computed(() => {
return {}
@@ -486,6 +491,7 @@ const openShare = async (row: any) => {
shareFormData.weapp = share.weapp
shareDialogVisible.value = true
shareModalApi.open()
}
const shareEvent = async (formEl: FormInstance | undefined) => {
@@ -499,11 +505,13 @@ const shareEvent = async (formEl: FormInstance | undefined) => {
}).then(() => {
loadDiyFormList(getTablePageStorage(diyFormTableData.searchParam).page)
shareDialogVisible.value = false
shareModalApi.close()
}).catch(() => {
})
}
})
}
const cancelShare = () => { shareDialogVisible.value = false; shareModalApi.close() }
// 表单推广
const spreadPopupRef = ref(null)

View File

@@ -150,7 +150,7 @@
</el-tab-pane>
</el-tabs>
<el-dialog v-model="dialogVisible" :title="t('viewInformation')" width="400px">
<ModalDiyFormView :class="'w-[400px]'" :title="t('viewInformation')">
<div class="flex flex-col">
<div class="flex mb-[10px]" v-for="(item, index) in formDetail" :key="index">
<div class="flex justify-end w-[100px]">{{ item.label }}</div>
@@ -167,10 +167,10 @@
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="dialogVisible = false">{{ t('confirm') }}</el-button>
<el-button type="primary" @click="closeView">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalDiyFormView>
</el-drawer>
<export-sure ref="exportSureDialog" :show="flag" type="diy_form_records" :searchParam="formData.searchParam" @close="handleExportClose" />
@@ -190,6 +190,7 @@ const router = useRouter()
const showDialog = ref(false)
const activeName = ref('detail_data')
const formId = ref(0)
const [ModalDiyFormView, viewModalApi] = useVbenModal()
const dialogVisible = ref(false)
const searchFormDiyFormRef = ref<FormInstance>()
const searchFormDiyMemberRef = ref<FormInstance>()
@@ -241,8 +242,10 @@ const formDetailEvent = (row: any) => {
getFormRecordsInfo(row.record_id).then((res:any) => {
formDetail.value = res.data.value
dialogVisible.value = true
viewModalApi.open()
})
}
const closeView = () => { dialogVisible.value = false; viewModalApi.close() }
// 删除
const deleteEvent = (row: any) => {
@@ -388,3 +391,4 @@ defineExpose({
}
}
</style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -95,7 +95,7 @@
</div>
</el-card>
<el-dialog v-model="showDialog" :title="t('accountDetail')" width="550px" :destroy-on-close="true">
<ModalAccountDetail :class="'w-[550px]'" :title="t('accountDetail')">
<el-form :model="formData" label-width="110px" ref="formRef" class="page-form">
<!-- <el-form-item :label="t('tradeNo')">-->
@@ -159,10 +159,10 @@
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button>
<el-button type="primary" @click="cancel">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalAccountDetail>
</div>
</template>
@@ -232,6 +232,7 @@ const checkAccountType = () => {
})
}
checkAccountType()
const [ModalAccountDetail, accountDetailModalApi] = useVbenModal()
const showDialog = ref(false)
const formData = ref({
trade_no: '',
@@ -253,8 +254,10 @@ const formData = ref({
})
const detailEvent = (info:any) => {
showDialog.value = true
accountDetailModalApi.open()
formData.value = info
}
const cancel = () => { showDialog.value = false; accountDetailModalApi.close() }
interface AccountStat{
pay: number,
@@ -274,3 +277,4 @@ checkAccountStat()
</script>
<style lang="scss" scoped></style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -207,7 +207,7 @@
</el-card>
<!-- 详情 -->
<el-dialog v-model="cashOutShowDialog" :title="t('cashOutDetail')" width="650px" :destroy-on-close="true">
<ModalDetail :class="'w-[650px]'" :title="t('cashOutDetail')">
<el-form :model="cashOutInfo" label-width="120px" ref="formRef" class="page-form" v-loading="cashOutLoading">
<el-row>
<el-col :span="12">
@@ -309,12 +309,12 @@
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="cashOutShowDialog = false">{{ t('confirm') }}</el-button>
<el-button type="primary" @click="detailClose">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalDetail>
<!-- 审核通过 -->
<el-dialog v-model="auditPassShowDialog" :title="t('passAudit')" width="650px" :destroy-on-close="true">
<ModalAuditPass :class="'w-[650px]'" :title="t('passAudit')">
<el-form :model="curData" label-width="120px" ref="formRef" class="page-form">
<el-row>
<el-col :span="12">
@@ -390,13 +390,13 @@
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="auditPassShowDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="auditPassCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="handlePass()">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalAuditPass>
<!-- 是否审核拒绝 -->
<el-dialog v-model="auditShowDialog" :title="t('rejectionAudit')" width="500px" :destroy-on-close="true">
<ModalAuditRefuse :class="'w-[500px]'" :title="t('rejectionAudit')">
<el-form :model="auditFailure" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('reasonsRefusal')" prop="refuse_reason">
<el-input v-model.trim="auditFailure.refuse_reason" clearable maxlength="200" :show-word-limit="true" :placeholder="t('reasonsRefusalPlaceholder')" :rows="4" class="input-width" type="textarea" />
@@ -404,14 +404,14 @@
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="auditShowDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="auditRefuseCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalAuditRefuse>
<!-- 是否转账 -->
<el-dialog v-model="transferShowDialog" :title="t('transfer')" width="650px" :destroy-on-close="true">
<ModalTransfer :class="'w-[650px]'" :title="t('transfer')">
<el-form :model="transferData" label-width="120px" ref="formRef" class="page-form">
<el-row>
<template v-if="transferData.transfer_type == 'alipay' || transferData.transfer_type == 'wechat_code'">
@@ -476,13 +476,13 @@
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="transferShowDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="transferCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="handleTransfer(formTransferRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalTransfer>
<!-- 备注 -->
<el-dialog v-model="remarkShowDialog" :title="t('remark')" width="500px" :destroy-on-close="true">
<ModalRemark :class="'w-[500px]'" :title="t('remark')">
<el-form :model="formData" label-width="90px" ref="formRemarkRef" :rules="formRemarkRules" class="page-form">
<el-form-item :label="t('remark')" prop="remark">
<el-input v-model.trim="formData.remark" type="textarea" rows="4" clearable
@@ -491,11 +491,11 @@
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="remarkShowDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="remarkCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="save(formRemarkRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalRemark>
</div>
</template>
@@ -506,6 +506,7 @@ import { getCashOutList, getTransfertype, memberTransfer, memberAudit, getCashOu
import { img } from '@/utils/common'
import { ElMessageBox, FormInstance, FormRules } from 'element-plus'
import { useRouter, useRoute } from 'vue-router'
import { useVbenModal } from '@vben/common-ui'
const cashOutStatusList = ref([])
const checkStatusList = async () => {
@@ -608,28 +609,29 @@ const formTransferRules = computed(() => {
const transferFn = (data:any) => {
transferData.value = data
formTransfer.id = data.id
transferShowDialog.value = true
modalTransferApi.open()
}
const handleTransfer = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
memberTransfer({ ...formTransfer }).then(res => {
transferShowDialog.value = false
modalTransferApi.close()
loadOrderList()
}).catch(() => {
transferShowDialog.value = false
modalTransferApi.close()
loadOrderList()
})
}
})
}
const transferCancel = () => { modalTransferApi.close() }
/**
* 详情
* @param data
*/
const cashOutShowDialog = ref(false)
const [ModalDetail, modalDetailApi] = useVbenModal()
const cashOutInfo = ref({
nickname: '',
account_type_name: '',
@@ -643,25 +645,26 @@ const cashOutLoading = ref(true)
const detailFn = (id:any) => {
getCashOutDetail(id).then(res => {
cashOutInfo.value = res.data
cashOutShowDialog.value = true
modalDetailApi.open()
cashOutLoading.value = false
}).catch(() => {
loadOrderList()
})
}
const detailClose = () => { modalDetailApi.close() }
/**
* 提现审核
* @param data
*/
const auditPassShowDialog = ref(false)
const [ModalAuditPass, modalAuditPassApi] = useVbenModal()
const curData = ref<any>({})
// 审核成功弹框
const successfulAuditFn = (data: any) => {
curData.value = data
auditPassShowDialog.value = true
modalAuditPassApi.open()
}
const handlePass = () => {
const obj = {
@@ -670,24 +673,26 @@ const handlePass = () => {
}
cashOutAuditFn(obj)
}
const auditPassCancel = () => { modalAuditPassApi.close() }
/**
* 拒绝审核
*/
const auditFailure = ref({ refuse_reason: '', id: 0, action: '' })
const auditShowDialog = ref(false)
const [ModalAuditRefuse, modalAuditRefuseApi] = useVbenModal()
const loading = ref(false)
const auditFailureFn = (data: any) => {
auditFailure.value.id = data.id
auditFailure.value.action = 'refuse'
auditShowDialog.value = true
modalAuditRefuseApi.open()
}
const confirm = () => {
auditShowDialog.value = false
modalAuditRefuseApi.close()
cashOutAuditFn(auditFailure.value)
}
const auditRefuseCancel = () => { modalAuditRefuseApi.close() }
const repeat = ref(false)
const cashOutAuditFn = (data:any) => {
@@ -726,7 +731,7 @@ const memberCancelFn = (data: any) => {
*/
const formRemarkRef = ref<FormInstance>()
const remarkShowDialog = ref(false)
const [ModalRemark, modalRemarkApi] = useVbenModal()
const formData = reactive({
id: 0,
remark: ''
@@ -741,7 +746,7 @@ const formRemarkRules = computed(() => {
const handleRemark = (data: any) => {
formData.id = data.id
formData.remark = ''
remarkShowDialog.value = true
modalRemarkApi.open()
}
const save = async (formEl: FormInstance | undefined) => {
if (!formEl) return
@@ -749,13 +754,14 @@ const save = async (formEl: FormInstance | undefined) => {
if (valid) {
memberRemark(formData).then((res: any) => {
loadOrderList()
remarkShowDialog.value = false
modalRemarkApi.close()
}).catch(() => {
remarkShowDialog.value = false
modalRemarkApi.close()
})
}
})
}
const remarkCancel = () => { modalRemarkApi.close() }
/**
* 会员详情
*/

View File

@@ -25,7 +25,7 @@
</el-table>
</div>
<el-dialog v-model="transferDialog" :title="title" width="500px" class="diy-dialog-wrap" :destroy-on-close="true">
<ModalRefund :class="'w-[500px]'" :title="t('transfer')">
<el-form :model="transferFormData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('transferType')">
<el-radio-group v-model="transferFormData.refund_type">
@@ -42,11 +42,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="transferDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="refundCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalRefund>
</div>
</el-drawer>
</template>
@@ -57,6 +57,7 @@ import { t } from '@/lang'
import { getPayRefundInfo, getRefundType, getRefundTransfer } from '@/app/api/pay'
import { FormInstance } from 'element-plus'
import { useRouter, useRoute } from 'vue-router'
import { useVbenModal } from '@vben/common-ui'
const route = useRoute()
const router = useRouter()
@@ -101,9 +102,9 @@ getRefundType().then((data) => {
})
})
const transferDialog = ref(false)
const [ModalRefund, modalRefundApi] = useVbenModal()
const transferEvent = (data:any) => {
transferDialog.value = true
modalRefundApi.open()
transferFormData.refund_no = data.refund_no
transferFormData.refund_money = data.money
transferFormData.voucher = ''
@@ -136,17 +137,18 @@ const confirm = async (formEl: FormInstance | undefined) => {
const data = transferFormData
getRefundTransfer(data).then(res => {
loading.value = false
transferDialog.value = false
modalRefundApi.close()
refundList.value = []
getRefundListInfo(refundNo)
emit('loadPayRefundList')
}).catch(() => {
transferDialog.value = false
modalRefundApi.close()
loading.value = false
})
}
})
}
const refundCancel = () => { modalRefundApi.close() }
defineExpose({
showDialog,
setFormData

View File

@@ -30,7 +30,7 @@
</el-table>
</el-card>
<el-dialog v-model="transferDialog" :title="title" width="500px" class="diy-dialog-wrap" :destroy-on-close="true">
<ModalRefund :class="'w-[500px]'" :title="t('transfer')">
<el-form :model="transferFormData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('transferType')">
<el-radio-group v-model="transferFormData.refund_type">
@@ -47,11 +47,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="transferDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="refundCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalRefund>
</div>
</template>
@@ -62,6 +62,7 @@ import { getPayRefundInfo, getRefundType, getRefundTransfer } from '@/app/api/pa
import { useRoute, useRouter } from 'vue-router'
import { FormInstance } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import { useVbenModal } from '@vben/common-ui'
const route = useRoute()
const router = useRouter()
@@ -95,9 +96,9 @@ getRefundType().then((data) => {
})
})
const transferDialog = ref(false)
const [ModalRefund, modalRefundApi] = useVbenModal()
const transferEvent = (data:any) => {
transferDialog.value = true
modalRefundApi.open()
transferFormData.refund_no = data.refund_no
transferFormData.refund_money = data.money
}
@@ -130,16 +131,17 @@ const confirm = async (formEl: FormInstance | undefined) => {
const data = transferFormData
getRefundTransfer(data).then(res => {
loading.value = false
transferDialog.value = false
modalRefundApi.close()
refundList.value = []
setFormData(refundNo)
}).catch(() => {
transferDialog.value = false
modalRefundApi.close()
loading.value = false
})
}
})
}
const refundCancel = () => { modalRefundApi.close() }
</script>
<style lang="scss" scoped></style>

View File

@@ -207,7 +207,7 @@
</el-empty>
</div>
<el-dialog v-model="authCodeApproveDialog" title="授权码认证" width="400px">
<ModalAuthApprove :class="'w-[400px]'" title="授权码认证">
<el-form :model="formData" label-width="0" ref="formRef" :rules="formRules" class="page-form">
<el-card class="box-card !border-none" shadow="never">
<el-form-item prop="auth_code">
@@ -231,9 +231,9 @@
</div>
</el-card>
</el-form>
</el-dialog>
</ModalAuthApprove>
<!-- 详情 -->
<el-dialog v-model="appStoreShowDialog" :title="t('plugDetail')" width="500px" :destroy-on-close="true">
<ModalAppStoreInfo :class="'w-[500px]'" :title="t('plugDetail')">
<el-form :model="appStoreInfo" label-width="120px" ref="formRef" class="page-form">
<el-form-item :label="t('title')">
<div class="input-width">{{ appStoreInfo.title }}</div>
@@ -250,13 +250,13 @@
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="appStoreShowDialog = false">{{ t("confirm") }}</el-button>
<el-button type="primary" @click="appStoreClose">{{ t("confirm") }}</el-button>
</span>
</template>
</el-dialog>
</ModalAppStoreInfo>
<!-- 安装弹窗 -->
<el-dialog v-model="installShowDialog" :title="t('addonInstall')" width="850px" :close-on-click-modal="false" :close-on-press-escape="false" :before-close="installShowDialogClose">
<ModalInstall :class="'w-[850px]'" :title="t('addonInstall')" :close-on-click-modal="false" :close-on-press-escape="false" @close="installModalClose">
<el-steps :space="200" :active="installStep" class="number-of-steps" process-status="process" align-center v-if="installStep != 2 && !errorDialog ">
<el-step :title="t('envCheck')" class="flex-1" />
<el-step :title="t('installProgress')" class="flex-1" />
@@ -369,8 +369,8 @@
</div>
<div class="text-[16px] text-[#9699B6] mt-[10px]" v-if="upgradeDuration>0">本次安装用时{{ formatUpgradeDuration }}</div>
<div class="mt-[20px]">
<el-button @click="handleBack()" v-if="installType=='cloud'" class="!w-[90px]">返回</el-button>
<el-button @click="installShowDialog=false" type="primary" class="!w-[90px]">完成</el-button>
<el-button @click="handleBack()" v-if="installType=='cloud'" class="!w-[90px]">返回</el-button>
<el-button @click="installModalFinish" type="primary" class="!w-[90px]">完成</el-button>
</div>
</template>
</el-result>
@@ -385,13 +385,13 @@
{{errorMsg}}
</el-scrollbar>
<el-button @click="handleBack()" v-if="installType=='cloud'" class="!w-[90px]">错误信息</el-button>
<el-button @click="installShowDialog=false" type="primary" class="!w-[90px]">完成</el-button>
<el-button @click="installModalFinish" type="primary" class="!w-[90px]">完成</el-button>
</template>
</el-result>
</div>
</el-dialog>
</ModalInstall>
<el-dialog v-model="uninstallShowDialog" :title="t('addonUninstall')" width="850px" :close-on-click-modal="false" :close-on-press-escape="false">
<ModalUninstallCheck :class="'w-[850px]'" :title="t('addonUninstall')" :close-on-click-modal="false" :close-on-press-escape="false">
<el-scrollbar max-height="50vh">
<div class="min-h-[150px]">
<div class="bg-[#fff] my-3" v-if="uninstallCheckResult.dir">
@@ -452,18 +452,18 @@
</div>
</div>
</el-scrollbar>
</el-dialog>
</ModalUninstallCheck>
<!-- 下载提示 -->
<el-dialog v-model="unloadHintDialog" title="下载提示" width="30%">
<ModalUnloadHint :class="'w-[30%]'" title="下载提示">
<span>本地已经存在该插件/应用再次下载会覆盖该插件/应用</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="unloadHintDialog = false">取消</el-button>
<el-button @click="unloadHintCancel">取消</el-button>
<el-button type="primary" @click="downEventHintFn">确定</el-button>
</span>
</template>
</el-dialog>
</ModalUnloadHint>
<!-- 更新信息 -->
</el-card>
</div>
@@ -760,6 +760,7 @@ const installAddonFn = (key: string) => {
errorDialog.value = false
installType.value = ''
installShowDialog.value = true
installModalApi.open()
installAfterTips.value = []
installCheckResult.value = res.data
userStore.clearRouters()
@@ -857,6 +858,7 @@ const formatUpgradeDuration = computed(() => {
const checkInstallTask = () => {
installShowDialog.value = true
installModalApi.open()
installStep.value = 1
}
@@ -1063,6 +1065,7 @@ const appStoreInfo = ref({})
const getAddonDetailFn = (data: any) => {
appStoreShowDialog.value = true
appStoreInfo.value = data
appStoreModalApi.open()
}
// 更新信息

View File

@@ -116,7 +116,7 @@
</div>
</el-card>
<el-dialog v-model="developerDialogVisible" class="developer-dialog-wrap" title="开发人员模式说明" width="30%">
<ModalDeveloperTips :class="'w-[30%] developer-dialog-wrap'" title="开发人员模式说明">
<div>
<p class="text-[16px] mb-[4px]">开发模式</p>
<div class="text-[14px] indent-[2em]">开发人员模式即软件开发环境指框架开启了开发模式(DEBUG=TRUE) 开发模式时会出现开发选项卡仅用于开发人员使用包括应用及插件的安装卸载系统升级等等本菜单及子项功能均不受系统管理和权限控制</div>
@@ -125,10 +125,10 @@
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="developerDialogVisible = false">确定</el-button>
<el-button @click="developerCancel">确定</el-button>
</span>
</template>
</el-dialog>
</ModalDeveloperTips>
</div>
</template>
@@ -136,12 +136,13 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import useSystemStore from '@/stores/modules/system'
import { useVbenModal } from '@vben/common-ui'
const systemStore = useSystemStore()
systemStore.setHeadMenu('')
const router = useRouter()
const developerDialogVisible = ref(false)
const [ModalDeveloperTips, developerModalApi] = useVbenModal()
const toLink = (link:any) => {
router.push(link)
@@ -149,6 +150,7 @@ const toLink = (link:any) => {
const goRouter = () => {
window.open('https://www.niucloud.com/app')
}
const developerCancel = () => { developerModalApi.close() }
</script>
<style lang="scss" scoped>

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="popTitle" width="500px" :destroy-on-close="true">
<ModalAddMember :class="'w-[500px]'" :title="popTitle">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('memberNo')" prop="member_no">
@@ -25,11 +25,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</ModalAddMember>
</template>
<script lang="ts" setup>
@@ -39,6 +39,7 @@ import type { FormInstance } from 'element-plus'
import { addMember, getMemberList, getMemberNo } from '@/app/api/member'
import { filterNumber } from '@/utils/common'
const [ModalAddMember, addMemberModalApi] = useVbenModal()
const showDialog = ref(false)
const loading = ref(false)
const repeat = ref(false)
@@ -142,6 +143,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
loading.value = false
repeat.value = false
showDialog.value = false
addMemberModalApi.close()
emit('complete')
}).catch(() => {
loading.value = false
@@ -175,6 +177,9 @@ defineExpose({
showDialog,
setFormData
})
const cancel = () => { showDialog.value = false; addMemberModalApi.close() }
</script>
<style lang="scss" scoped></style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="popTitle" width="500px" :destroy-on-close="true">
<ModalEditLabel :class="'w-[500px]'" :title="popTitle">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('labelName')" prop="label_name">
<el-input v-model.trim="formData.label_name" clearable :placeholder="t('labelNamePlaceholder')" class="input-width" />
@@ -15,11 +15,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</ModalEditLabel>
</template>
<script lang="ts" setup>
@@ -29,6 +29,7 @@ import type { FormInstance } from 'element-plus'
import { addMemberLabel, updateMemberLabel, getMemberLabelInfo } from '@/app/api/member'
import { filterNumber } from '@/utils/common'
const [ModalEditLabel, editLabelModalApi] = useVbenModal()
const showDialog = ref(false)
const loading = ref(false)
const repeat = ref(false)
@@ -92,6 +93,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
loading.value = false
repeat.value = false
showDialog.value = false
editLabelModalApi.close()
emit('complete')
}).catch(() => {
loading.value = false
@@ -122,6 +124,9 @@ defineExpose({
showDialog,
setFormData
})
const cancel = () => { showDialog.value = false; editLabelModalApi.close() }
</script>
<style lang="scss" scoped></style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="title || t('updateMember')" width="500px" :destroy-on-close="true">
<ModalEditMember :class="'w-[500px]'" :title="title || t('updateMember')">
<el-form :model="saveData" label-width="90px" :rules="formRules" ref="formRef" class="page-form" @submit.prevent v-loading="loading">
<el-form-item :label="t('headimg')" v-if="type == 'headimg'">
@@ -43,12 +43,12 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" v-if="method=='batchSet'" @click="batchSetConfirm(formRef)">{{t('confirm')}}</el-button>
<el-button type="primary" :loading="loading" v-else @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</ModalEditMember>
</template>
<script lang="ts" setup>
@@ -64,6 +64,7 @@ const type = ref('')
const title = ref('')
// 会员id
const memberId = ref('')
const [ModalEditMember, editMemberModalApi] = useVbenModal()
const showDialog = ref(false)
const loading = ref(false)
const repeat = ref(false)
@@ -187,6 +188,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
loading.value = false
repeat.value = false
showDialog.value = false
editMemberModalApi.close()
emit('complete')
}).catch(() => {
loading.value = false
@@ -265,6 +267,7 @@ const batchSetConfirm = async (formEl: FormInstance | undefined) => {
loading.value = false
repeat.value = false
showDialog.value = false
editMemberModalApi.close()
emit('complete')
}).catch(() => {
loading.value = false
@@ -279,6 +282,9 @@ defineExpose({
setDialogType,
batchSetDialogType
})
const cancel = () => { showDialog.value = false; editMemberModalApi.close() }
</script>
<style lang="scss" scoped></style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="t('adjustBalance')" width="550px" :destroy-on-close="true">
<ModalBalanceEdit :class="'w-[550px]'" :title="t('adjustBalance')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading" @submit.enter.prevent>
<el-form-item :label="t('currBalance')" >
@@ -25,11 +25,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</ModalBalanceEdit>
</template>
<script lang="ts" setup>
@@ -37,8 +37,9 @@ import { ref, reactive, computed } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { adjustBalance } from '@/app/api/member'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [ModalBalanceEdit, modalApi] = useVbenModal()
const loading = ref(true)
const repeat = ref(false)
@@ -102,7 +103,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
adjustBalance(data).then(res => {
loading.value = false
repeat.value = false
showDialog.value = false
modalApi.close()
emit('complete')
}).catch(() => {
loading.value = false
@@ -123,12 +124,11 @@ const setFormData = async (row: any = null) => {
})
}
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="t('balanceInfo')" width="550px" :destroy-on-close="true">
<ModalBalanceInfo :class="'w-[550px]'" :title="t('balanceInfo')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('headimg')">
@@ -41,10 +41,10 @@
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button>
<el-button type="primary" @click="cancel">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalBalanceInfo>
</template>
<script lang="ts" setup>
@@ -52,8 +52,9 @@ import { ref, reactive, computed } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [ModalBalanceInfo, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -98,12 +99,11 @@ const setFormData = async (row: any = null) => {
}
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="t('moneyInfo')" width="550px" :destroy-on-close="true">
<ModalCommissionInfo :class="'w-[550px]'" :title="t('moneyInfo')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('headimg')">
@@ -41,10 +41,10 @@
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button>
<el-button type="primary" @click="cancel">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalCommissionInfo>
</template>
<script lang="ts" setup>
@@ -52,8 +52,9 @@ import { ref, reactive, computed } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [ModalCommissionInfo, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -98,12 +99,11 @@ const setFormData = async (row: any = null) => {
}
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="t('moneyInfo')" width="550px" :destroy-on-close="true">
<ModalMoneyInfo :class="'w-[550px]'" :title="t('moneyInfo')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('headimg')" >
@@ -37,10 +37,10 @@
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button>
<el-button type="primary" @click="cancel">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalMoneyInfo>
</template>
<script lang="ts" setup>
@@ -48,8 +48,9 @@ import { ref, reactive, computed } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [ModalMoneyInfo, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -91,12 +92,11 @@ const setFormData = async (row: any = null) => {
}
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="t('adjustPoint')" width="550px" :destroy-on-close="true">
<ModalPointEdit :class="'w-[550px]'" :title="t('adjustPoint')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading" @submit.enter.prevent>
<el-form-item :label="t('currPoint')" >
@@ -25,11 +25,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</ModalPointEdit>
</template>
<script lang="ts" setup>
@@ -37,8 +37,9 @@ import { ref, reactive, computed } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { adjustPoint } from '@/app/api/member'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [ModalPointEdit, modalApi] = useVbenModal()
const loading = ref(true)
const repeat = ref(false)
@@ -104,7 +105,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
adjustPoint(data).then(res => {
loading.value = false
repeat.value = false
showDialog.value = false
modalApi.close()
emit('complete')
}).catch(() => {
loading.value = false
@@ -126,12 +127,11 @@ const setFormData = async (row: any = null) => {
})
}
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="t('pointInfo')" width="550px" :destroy-on-close="true">
<ModalPointInfo :class="'w-[550px]'" :title="t('pointInfo')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('headimg')" >
@@ -41,10 +41,10 @@
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button>
<el-button type="primary" @click="cancel">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalPointInfo>
</template>
<script lang="ts" setup>
@@ -52,8 +52,9 @@ import { ref, reactive, computed } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [ModalPointInfo, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -97,12 +98,11 @@ const setFormData = async (row: any = null) => {
}
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -6,22 +6,15 @@
</el-card>
<el-card class="box-card mt-[15px] !border-none" shadow="never" v-loading="loading">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
<el-form-item :label="t('type')">
<el-input v-model.trim="formData.agreement_key_name" readonly class="input-width" />
</el-form-item>
<el-form-item :label="t('title')" prop="title">
<el-input v-model.trim="formData.title" clearable :placeholder="t('titlePlaceholder')" class="input-width" maxlength="20" />
</el-form-item>
<el-form-item :label="t('content')" prop="content">
<editor v-model="formData.content" />
</el-form-item>
</el-form>
<BaseForm />
<el-form-item :label="t('content')">
<editor v-model="formModel.content" />
</el-form-item>
</el-card>
<div class="fixed-footer-wrap">
<div class="fixed-footer">
<el-button type="primary" @click="onSave(formRef)">{{ t('save') }}</el-button>
<el-button type="primary" @click="onSave()">{{ t('save') }}</el-button>
<el-button @click="back()">{{ t('cancel') }}</el-button>
</div>
</div>
@@ -29,13 +22,13 @@
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import { getAgreementInfo, editAgreement } from '@/app/api/sys'
import { useRoute, useRouter } from 'vue-router'
import useTabbarStore from '@/stores/modules/tabbar'
import { useVbenForm } from '@/_env/adapter/form'
const route = useRoute()
const router = useRouter()
@@ -48,67 +41,42 @@ const pageName = route.meta.title
* 表单数据
*/
const initialFormData = {
agreement_key: '',
content: '',
title: '',
agreement_key_name: ''
agreement_key: '',
content: '',
title: '',
agreement_key_name: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formModel: Record<string, any> = reactive({ ...initialFormData })
loading.value = true
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
if (!formModel.content || formModel.content.length < 5 || formModel.content.length > 100000) return
loading.value = true
const data = { ...values, content: formModel.content, key: values.agreement_key }
editAgreement(data).then(() => { loading.value = false; back() }).catch(() => { loading.value = false })
},
layout: 'horizontal',
schema: [
{ component: 'Input', fieldName: 'agreement_key_name', label: t('type'), componentProps: { readonly: true } },
{ component: 'Input', fieldName: 'title', label: t('title'), rules: [{ required: true }] }
],
wrapperClass: 'grid-cols-1'
})
const setFormData = async (agreement_key: string = '') => {
Object.assign(formData, initialFormData)
const data = await (await getAgreementInfo(agreement_key)).data
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
loading.value = false
Object.assign(formModel, initialFormData)
const data = await (await getAgreementInfo(agreement_key)).data
Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] })
formApi.setModel({ agreement_key_name: formModel.agreement_key_name, title: formModel.title, agreement_key: agreement_key })
loading.value = false
}
if (agreement_key) setFormData(agreement_key)
const formRef = ref<FormInstance>()
const onSave = async () => { if (loading.value) return; formApi.submit() }
// 表单验证规则
const formRules = computed(() => {
return {
title: [
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' }
],
content: [
{
required: true,
trigger: ['blur', 'change'],
validator: (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error(t('contentPlaceholder')))
} else if (value.length < 5 || value.length > 100000) {
callback(new Error(t('contentMaxTips')))
return false
} else {
callback()
}
}
}
]
}
})
const onSave = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
data.key = formData.agreement_key
editAgreement(data).then(res => {
loading.value = false
back()
}).catch(() => {
loading.value = false
})
}
})
}
// editor内容的校验已在 handleSubmit 中做长度校验
const back = () => {
tabbarStore.removeTab(route.path)

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="t('cronInfo')" width="550px" :destroy-on-close="true">
<ModalCronInfo :class="'w-[550px]'" :title="t('cronInfo')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('title')" >
@@ -47,18 +47,19 @@
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button>
<el-button type="primary" @click="cancel">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalCronInfo>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [ModalCronInfo, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -100,12 +101,11 @@ const setFormData = async (row: any = null) => {
})
}
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,51 +1,40 @@
<template>
<el-dialog v-model="showDialog" :title="t('messageInfo')" width="550px" :destroy-on-close="true">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('messageKey')">
<div class="input-width"> {{ formData.name }} </div>
</el-form-item>
<el-form-item :label="t('smsType')">
<div class="input-width">
<div v-if="formData.notice_type == 'sms'">{{ t('sms') }}</div>
<div v-if="formData.notice_type == 'wechat'">{{ t('wechat') }}</div>
<div v-if="formData.notice_type == 'weapp'">{{ t('weapp') }}</div>
</div>
</el-form-item>
<!-- <el-form-item :label="t('messageData')">
<div class="input-width"> {{ formData.message_data }} </div>
</el-form-item> -->
<el-form-item :label="t('nickname')">
<div class="input-width"> {{ formData.nickname }} </div>
</el-form-item>
<el-form-item :label="t('receiver')">
<div class="input-width"> {{ formData.receiver }} </div>
</el-form-item>
<el-form-item :label="t('createTime')">
<div class="input-width"> {{ formData.create_time }} </div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
<Modal :class="'w-[550px]'" :title="t('messageInfo')">
<div class="page-form" v-loading="loading">
<el-form-item :label="t('messageKey')">
<div class="input-width"> {{ formModel.name }} </div>
</el-form-item>
<el-form-item :label="t('smsType')">
<div class="input-width">
<div v-if="formModel.notice_type == 'sms'">{{ t('sms') }}</div>
<div v-if="formModel.notice_type == 'wechat'">{{ t('wechat') }}</div>
<div v-if="formModel.notice_type == 'weapp'">{{ t('weapp') }}</div>
</div>
</el-form-item>
<el-form-item :label="t('nickname')">
<div class="input-width"> {{ formModel.nickname }} </div>
</el-form-item>
<el-form-item :label="t('receiver')">
<div class="input-width"> {{ formModel.receiver }} </div>
</el-form-item>
<el-form-item :label="t('createTime')">
<div class="input-width"> {{ formModel.create_time }} </div>
</el-form-item>
</div>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="close()">{{ t('confirm') }}</el-button>
</span>
</template>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -61,34 +50,21 @@ const initialFormData = {
receiver: '',
notice_type: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
}
})
const formModel: Record<string, any> = reactive({ ...initialFormData })
const setFormData = async (row: any = null) => {
loading.value = true
Object.assign(formData, initialFormData)
if (row) {
Object.keys(formData).forEach((key: string) => {
if (row[key] != undefined) formData[key] = row[key]
})
}
loading.value = false
loading.value = true
Object.assign(formModel, initialFormData)
if (row) {
Object.keys(formModel).forEach((key: string) => { if (row[key] != undefined) formModel[key] = row[key] })
}
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const close = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,121 +1,82 @@
<template>
<el-dialog v-model="showDialog" :title="t('noticeSetting')" width="550px" :destroy-on-close="true">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('status')">
<el-radio-group v-model="formData.is_sms">
<el-radio :label="1">{{ t('startUsing') }}</el-radio>
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('name')">
<div class="input-width"> {{ formData.name }} </div>
</el-form-item>
<el-form-item :label="t('title')">
<div class="input-width"> {{ formData.title }} </div>
</el-form-item>
<el-form-item :label="t('smsId')" prop="sms_id">
<el-input v-model.trim="formData.sms_id" :placeholder="t('smsIdPlaceholder')" class="input-width" show-word-limit clearable />
</el-form-item>
<el-form-item :label="t('smsContent')">
<div class="input-width"> {{ formData.content }} </div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
<Modal :class="'w-[550px]'" :title="t('noticeSetting')">
<BaseForm />
<el-form-item :label="t('name')">
<div class="input-width"> {{ formModel.name }} </div>
</el-form-item>
<el-form-item :label="t('title')">
<div class="input-width"> {{ formModel.title }} </div>
</el-form-item>
<el-form-item :label="t('smsContent')">
<div class="input-width"> {{ formModel.content }} </div>
</el-form-item>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span>
</template>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { editNotice } from '@/app/api/notice'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
/**
* 表单数据
*/
const initialFormData = {
is_sms: 0,
key: '',
name: '',
sms_default_content: '',
title: '',
type: '',
sms_id: '',
content: ''
is_sms: 0,
key: '',
name: '',
sms_default_content: '',
title: '',
type: '',
sms_id: '',
content: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
sms_id: [
{ required: true, message: t('smsIdPlaceholder'), trigger: 'blur' }
]
}
const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
loading.value = true
const data = { ...values, status: values.is_sms }
editNotice(data).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
},
layout: 'horizontal',
schema: [
{ component: 'RadioGroup', fieldName: 'is_sms', label: t('status'), componentProps: { options: [ { label: t('startUsing'), value: 1 }, { label: t('statusDeactivate'), value: 0 } ] } },
{ component: 'Input', fieldName: 'sms_id', label: t('smsId'), rules: [{ required: true }] }
],
wrapperClass: 'grid-cols-1'
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
data.status = data.is_sms
editNotice(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
// showDialog.value = false
})
}
})
}
const confirm = async () => { if (loading.value) return; formApi.submit() }
const cancel = () => { modalApi.close() }
const setFormData = async (row: any = null) => {
loading.value = true
Object.assign(formData, initialFormData)
if (row) {
Object.keys(formData).forEach((key: string) => {
if (row[key] != undefined) formData[key] = row[key]
if (row.sms && row.sms[key] != undefined) formData[key] = row.sms[key]
})
}
loading.value = false
loading.value = true
Object.assign(formModel, initialFormData)
if (row) {
Object.keys(formModel).forEach((key: string) => {
if (row[key] != undefined) formModel[key] = row[key]
if (row.sms && row.sms[key] != undefined) formModel[key] = row.sms[key]
})
}
formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,45 +1,34 @@
<template>
<el-dialog v-model="showDialog" :title="t('noticeSetting')" width="550px" :destroy-on-close="true">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('status')">
<el-radio-group v-model="formData.is_weapp">
<el-radio :label="1">{{ t('startUsing') }}</el-radio>
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('name')">
<div class="input-width"> {{ formData.name }} </div>
</el-form-item>
<el-form-item :label="t('weappTempKey')">
<div class="input-width"> {{ formData.tid }} </div>
</el-form-item>
<el-form-item :label="t('content')">
<div class="input-width">
<div v-for="(item, index) in formData.content" :key="index">{{ item[0] }}{{ item[1] }} </div>
</div>
</el-form-item>
</el-form>
<Modal :class="'w-[550px]'" :title="t('noticeSetting')">
<BaseForm />
<el-form-item :label="t('name')">
<div class="input-width"> {{ formModel.name }} </div>
</el-form-item>
<el-form-item :label="t('weappTempKey')">
<div class="input-width"> {{ formModel.tid }} </div>
</el-form-item>
<el-form-item :label="t('content')">
<div class="input-width">
<div v-for="(item, index) in formModel.content" :key="index">{{ item[0] }}{{ item[1] }} </div>
</div>
</el-form-item>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { editNoticeStatus } from '@/app/api/notice'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -52,66 +41,45 @@ const initialFormData = {
title: '',
type: '',
content: [],
first: '',
remark: '',
tid: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formModel: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
}
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
loading.value = true
const data = { ...values, status: values.is_weapp }
editNoticeStatus(data).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
},
layout: 'horizontal',
schema: [
{ component: 'RadioGroup', fieldName: 'is_weapp', label: t('status'), componentProps: { options: [ { label: t('startUsing'), value: 1 }, { label: t('statusDeactivate'), value: 0 } ] } }
],
wrapperClass: 'grid-cols-1'
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
data.status = data.is_weapp
editNoticeStatus(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
})
}
})
}
const confirm = async () => { if (loading.value) return; formApi.submit() }
const cancel = () => { modalApi.close() }
const setFormData = async (row: any = null) => {
loading.value = true
Object.assign(formData, initialFormData)
Object.assign(formModel, initialFormData)
if (row) {
Object.keys(formData).forEach((key: string) => {
if (row[key] != undefined) formData[key] = row[key]
if (row.weapp && row.weapp[key] != undefined) formData[key] = row.weapp[key]
Object.keys(formModel).forEach((key: string) => {
if (row[key] != undefined) formModel[key] = row[key]
if (row.weapp && row.weapp[key] != undefined) formModel[key] = row.weapp[key]
})
}
formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,57 +1,37 @@
<template>
<el-dialog v-model="showDialog" :title="t('noticeSetting')" width="550px" :destroy-on-close="true">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('status')">
<el-radio-group v-model="formData.is_wechat">
<el-radio :label="1">{{ t('startUsing') }}</el-radio>
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('name')">
<div class="input-width">{{ formData.name }} </div>
</el-form-item>
<el-form-item :label="t('tempKey')">
<div class="input-width">{{ formData.temp_key }} </div>
</el-form-item>
<el-form-item :label="t('keywordNameList')">
<div class="input-width">{{ formData.keyword_name_list ? formData.keyword_name_list.join('') : '' }} </div>
</el-form-item>
<!-- <el-form-item :label="t('first')" prop="first">-->
<!-- <el-input v-model.trim="formData.wechat_first" :placeholder="t('firstPlaceholder')" class="input-width" show-word-limit clearable />-->
<!-- </el-form-item>-->
<el-form-item :label="t('content')">
<div class="input-width">
<div v-for="(item, index) in formData.content" :key="index">{{ item[0] }}{{ item[1] }} </div>
</div>
</el-form-item>
<!-- <el-form-item :label="t('remark')" prop="remark">-->
<!-- <el-input v-model.trim="formData.wechat_remark" :placeholder="t('remarkPlaceholder')" class="input-width" show-word-limit clearable />-->
<!-- </el-form-item>-->
</el-form>
<Modal :class="'w-[550px]'" :title="t('noticeSetting')">
<BaseForm />
<el-form-item :label="t('name')">
<div class="input-width">{{ formModel.name }} </div>
</el-form-item>
<el-form-item :label="t('tempKey')">
<div class="input-width">{{ formModel.temp_key }} </div>
</el-form-item>
<el-form-item :label="t('keywordNameList')">
<div class="input-width">{{ formModel.keyword_name_list ? formModel.keyword_name_list.join('') : '' }} </div>
</el-form-item>
<el-form-item :label="t('content')">
<div class="input-width">
<div v-for="(item, index) in formModel.content" :key="index">{{ item[0] }}{{ item[1] }} </div>
</div>
</el-form-item>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { editNotice } from '@/app/api/notice'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -64,69 +44,45 @@ const initialFormData = {
title: '',
type: '',
content: [],
// first: '',
// remark: '',
temp_key: '',
keyword_name_list: ''
// wechat_first: '',
// wechat_remark: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formModel: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
}
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
loading.value = true
const data = { ...values, status: values.is_wechat }
editNotice(data).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
},
layout: 'horizontal',
schema: [
{ component: 'RadioGroup', fieldName: 'is_wechat', label: t('status'), componentProps: { options: [ { label: t('startUsing'), value: 1 }, { label: t('statusDeactivate'), value: 0 } ] } }
],
wrapperClass: 'grid-cols-1'
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
data.status = data.is_wechat
editNotice(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
})
}
})
}
const confirm = async () => { if (loading.value) return; formApi.submit() }
const cancel = () => { modalApi.close() }
const setFormData = async (row: any = null) => {
loading.value = true
Object.assign(formData, initialFormData)
Object.assign(formModel, initialFormData)
if (row) {
Object.keys(formData).forEach((key: string) => {
if (row[key] != undefined) formData[key] = row[key]
if (row.wechat && row.wechat[key] != undefined) formData[key] = row.wechat[key]
Object.keys(formModel).forEach((key: string) => {
if (row[key] != undefined) formModel[key] = row[key]
if (row.wechat && row.wechat[key] != undefined) formModel[key] = row.wechat[key]
})
// if (!row.wechat_first) formData['wechat_first'] = row['first']
// if (!row.wechat_remark) formData['wechat_remark'] = row['remark']
}
formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog v-model="showDialog" :title="t('updateAlipay')" width="550px" :destroy-on-close="true">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<Modal :class="'w-[550px]'" :title="t('updateAlipay')">
<BaseForm />
<el-form-item :label="t('appId')" prop="config.app_id">
<el-input v-model.trim="formData.config.app_id" :placeholder="t('appIdPlaceholder')" class="input-width" maxlength="32" show-word-limit clearable />
@@ -10,43 +10,42 @@
<el-input v-model.trim="formData.config.app_secret_cert" :placeholder="t('appSecretCertPlaceholder')" class="input-width" type="textarea" rows="4" clearable />
</el-form-item>
<el-form-item :label="t('appPublicCertPath')" prop="config.app_public_cert_path">
<el-form-item :label="t('appPublicCertPath')">
<div class="input-width">
<upload-file v-model.trim="formData.config.app_public_cert_path" api="sys/document/aliyun" />
<upload-file v-model.trim="formModel.config.app_public_cert_path" api="sys/document/aliyun" />
</div>
</el-form-item>
<el-form-item :label="t('alipayPublicCertPath')" prop="config.alipay_public_cert_path">
<el-form-item :label="t('alipayPublicCertPath')">
<div class="input-width">
<upload-file v-model="formData.config.alipay_public_cert_path" api="sys/document/aliyun" />
<upload-file v-model="formModel.config.alipay_public_cert_path" api="sys/document/aliyun" />
</div>
</el-form-item>
<el-form-item :label="t('alipayRootCertPath')" prop="config.alipay_root_cert_path">
<el-form-item :label="t('alipayRootCertPath')">
<div class="input-width">
<upload-file v-model="formData.config.alipay_root_cert_path" api="sys/document/aliyun" />
<upload-file v-model="formModel.config.alipay_root_cert_path" api="sys/document/aliyun" />
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import Test from '@/utils/test'
import { cloneDeep } from 'lodash-es'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
const initData = ref<any>(null)
/**
@@ -65,29 +64,22 @@ const initialFormData = {
status: 0,
is_default: 0
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
'config.app_id': [
{ required: true, message: t('appIdPlaceholder'), trigger: 'blur' }
],
'config.app_secret_cert': [
{ required: true, message: t('appSecretCertPlaceholder'), trigger: 'blur' }
],
'config.app_public_cert_path': [
{ required: true, message: t('appPublicCertPathPlaceholder'), trigger: 'blur' }
],
'config.alipay_public_cert_path': [
{ required: true, message: t('alipayPublicCertPathPlaceholder'), trigger: 'blur' }
],
'config.alipay_root_cert_path': [
{ required: true, message: t('alipayRootCertPathPlaceholder'), trigger: 'blur' }
]
}
const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
loading.value = true
const merged = { ...values, config: { ...formModel.config } }
emit('complete', merged)
loading.value = false
modalApi.close()
},
layout: 'horizontal',
schema: [
{ component: 'Input', fieldName: 'config.app_id', label: t('appId'), rules: [{ required: true }] },
{ component: 'Textarea', fieldName: 'config.app_secret_cert', label: t('appSecretCert'), rules: [{ required: true }], componentProps: { rows: 4 } }
],
wrapperClass: 'grid-cols-1'
})
const emit = defineEmits(['complete'])
@@ -96,50 +88,43 @@ const emit = defineEmits(['complete'])
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
emit('complete', formData)
showDialog.value = false
}
})
}
const confirm = async () => { if (loading.value) return; formApi.submit() }
const cancel = () => {
Object.assign(formData, initialFormData)
Object.assign(formModel, initialFormData)
if (initData.value) {
Object.keys(formData).forEach((key: string) => {
if (initData.value[key] != undefined) formData[key] = initData.value[key]
Object.keys(formModel).forEach((key: string) => {
if (initData.value[key] != undefined) formModel[key] = initData.value[key]
})
formData.channel = initData.value.redio_key.split('_')[0]
formData.status = Number(formData.status)
formModel.channel = initData.value.redio_key.split('_')[0]
formModel.status = Number(formModel.status)
}
emit('complete', formData)
showDialog.value = false
emit('complete', formModel)
modalApi.close()
}
const setFormData = async (data: any = null) => {
initData.value = cloneDeep(data)
loading.value = true
Object.assign(formData, initialFormData)
Object.assign(formModel, initialFormData)
if (data) {
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
Object.keys(formModel).forEach((key: string) => {
if (data[key] != undefined) formModel[key] = data[key]
})
formData.channel = data.redio_key.split('_')[0]
formData.status = Number(formData.status)
formModel.channel = data.redio_key.split('_')[0]
formModel.status = Number(formModel.status)
}
formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
}
const enableVerify = () => {
let verify = true
if (Test.empty(formData.config.app_id) || Test.empty(formData.config.app_secret_cert) || Test.empty(formData.config.app_public_cert_path) || Test.empty(formData.config.alipay_public_cert_path) || Test.empty(formData.config.alipay_root_cert_path)) verify = false
if (Test.empty(formModel.config.app_id) || Test.empty(formModel.config.app_secret_cert) || Test.empty(formModel.config.app_public_cert_path) || Test.empty(formModel.config.alipay_public_cert_path) || Test.empty(formModel.config.alipay_root_cert_path)) verify = false
return verify
}
defineExpose({
showDialog,
setFormData,
enableVerify
})

View File

@@ -1,58 +1,59 @@
<template>
<el-dialog v-model="showDialog" :title="formData.config.pay_type_name ? formData.config.pay_type_name : t('updateFriendsPay')" width="550px" :destroy-on-close="true">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<Modal :class="'w-[550px]'" :title="formModel.config.pay_type_name ? formModel.config.pay_type_name : t('updateFriendsPay')">
<BaseForm />
<el-form-item :label="t('friendsPaySwitch')">
<el-switch v-model="formData.config.pay_explain_switch" :active-value="1" :inactive-value="0"/>
<el-switch v-model="formModel.config.pay_explain_switch" :active-value="1" :inactive-value="0"/>
</el-form-item>
<template v-if="formData.config.pay_explain_switch == 1">
<el-form-item :label="t('friendsPayTitle')" prop="config.pay_explain_title">
<el-input v-model.trim="formData.config.pay_explain_title" :placeholder="t('friendsPayTitlePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
<template v-if="formModel.config.pay_explain_switch == 1">
<el-form-item :label="t('friendsPayTitle')">
<el-input v-model.trim="formModel.config.pay_explain_title" :placeholder="t('friendsPayTitlePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
</el-form-item>
<el-form-item :label="t('desContent')" prop="config.pay_explain_content">
<el-input v-model.trim="formData.config.pay_explain_content" :placeholder="t('desContentPlaceholder')" class="input-width" type="textarea" rows="4" maxlength="120" show-word-limit clearable />
<el-form-item :label="t('desContent')">
<el-input v-model.trim="formModel.config.pay_explain_content" :placeholder="t('desContentPlaceholder')" class="input-width" type="textarea" rows="4" maxlength="120" show-word-limit clearable />
</el-form-item>
</template>
<el-form-item :label="t('friendsPayGoodsSwitch')">
<div>
<el-switch v-model="formData.config.pay_info_switch" :active-value="1" :inactive-value="0"/>
<el-switch v-model="formModel.config.pay_info_switch" :active-value="1" :inactive-value="0"/>
<div class="text-[12px] text-[#999] leading-[20px]">{{ t('friendsPayGoodsSwitchTips') }}</div>
</div>
</el-form-item>
<el-form-item :label="t('friendsPayName')" prop="config.pay_type_name">
<el-input v-model.trim="formData.config.pay_type_name" :placeholder="t('friendsPayNamePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
<el-form-item :label="t('friendsPayName')">
<el-input v-model.trim="formModel.config.pay_type_name" :placeholder="t('friendsPayNamePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
</el-form-item>
<el-form-item :label="t('helpName')" prop="config.pay_page_name">
<el-input v-model.trim="formData.config.pay_page_name" :placeholder="t('helpNamePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
<el-form-item :label="t('helpName')">
<el-input v-model.trim="formModel.config.pay_page_name" :placeholder="t('helpNamePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
</el-form-item>
<el-form-item :label="t('helpBtn')" prop="config.pay_button_name">
<el-input v-model.trim="formData.config.pay_button_name" :placeholder="t('helpBtnPlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
<el-form-item :label="t('helpBtn')">
<el-input v-model.trim="formModel.config.pay_button_name" :placeholder="t('helpBtnPlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
</el-form-item>
<el-form-item :label="t('remark')" prop="config.pay_leave_message">
<el-input v-model.trim="formData.config.pay_leave_message" :placeholder="t('remarkPlaceholder')" class="input-width" type="textarea" rows="4" maxlength="20" show-word-limit clearable />
<el-form-item :label="t('remark')">
<el-input v-model.trim="formModel.config.pay_leave_message" :placeholder="t('remarkPlaceholder')" class="input-width" type="textarea" rows="4" maxlength="20" show-word-limit clearable />
</el-form-item>
<el-form-item :label="t('payWechatImage')" prop="config.pay_wechat_share_image" v-if="initData.redio_key == 'wechat_friendspay'">
<upload-image v-model="formData.config.pay_wechat_share_image" :limit="1" />
<el-form-item :label="t('payWechatImage')" v-if="initData.redio_key == 'wechat_friendspay'">
<upload-image v-model="formModel.config.pay_wechat_share_image" :limit="1" />
</el-form-item>
<el-form-item :label="t('payWeappImage')" prop="config.pay_weapp_share_image" v-if="initData.redio_key == 'weapp_friendspay'">
<upload-image v-model="formData.config.pay_weapp_share_image" :limit="1" />
<el-form-item :label="t('payWeappImage')" v-if="initData.redio_key == 'weapp_friendspay'">
<upload-image v-model="formModel.config.pay_weapp_share_image" :limit="1" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancel()">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import Test from '@/utils/test'
import { cloneDeep } from 'lodash-es'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
const initData = ref<any>(null)
/**
@@ -77,52 +78,23 @@ const initialFormData = {
status: 0,
is_default: 0
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
'config.pay_explain_title': [
{ required: true, message: t('friendsPayTitlePlaceholder'), trigger: 'blur' },
{
validator: (rule: any, value: string, callback: any) => {
if (formData.config.pay_explain_switch == 1 && value === '') {
callback(new Error(t('friendsPayTitlePlaceholder')))
}
callback()
},
trigger: 'blur'
}
],
'config.pay_explain_content': [
{ required: true, message: t('desContentPlaceholder'), trigger: 'blur' },
{
validator: (rule: any, value: string, callback: any) => {
if (formData.config.pay_explain_switch == 1 && value === '') {
callback(new Error(t('desContentPlaceholder')))
}
callback()
},
trigger: 'blur'
}
],
'config.pay_type_name': [
{ required: true, message: t('friendsPayNamePlaceholder'), trigger: 'blur' }
],
'config.pay_page_name': [
{ required: true, message: t('helpNamePlaceholder'), trigger: 'blur' }
],
'config.pay_button_name': [
{ required: true, message: t('helpBtnPlaceholder'), trigger: 'blur' }
],
'config.pay_leave_message': [
{ required: true, message: t('remarkPlaceholder'), trigger: 'blur' }
]
}
const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
loading.value = true
emit('complete', { ...values, config: { ...formModel.config } })
loading.value = false
modalApi.close()
},
layout: 'horizontal',
schema: [
{ component: 'Input', fieldName: 'config.pay_type_name', label: t('friendsPayName'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'config.pay_page_name', label: t('helpName'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'config.pay_button_name', label: t('helpBtn'), rules: [{ required: true }] },
{ component: 'Textarea', fieldName: 'config.pay_leave_message', label: t('remark'), rules: [{ required: true }] }
],
wrapperClass: 'grid-cols-1'
})
const emit = defineEmits(['complete'])
@@ -130,51 +102,40 @@ const emit = defineEmits(['complete'])
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
emit('complete', formData)
showDialog.value = false
}
})
}
const confirm = async () => { if (loading.value) return; formApi.submit() }
const cancel = () => {
Object.assign(formData, initialFormData)
if (initData.value) {
Object.keys(formData).forEach((key: string) => {
if (initData.value[key] != undefined) formData[key] = initData.value[key]
})
formData.channel = initData.value.redio_key.split('_')[0]
formData.status = Number(formData.status)
}
emit('complete', formData)
showDialog.value = false
Object.assign(formModel, initialFormData)
if (initData.value) {
Object.keys(formModel).forEach((key: string) => { if (initData.value[key] != undefined) formModel[key] = initData.value[key] })
formModel.channel = initData.value.redio_key.split('_')[0]
formModel.status = Number(formModel.status)
}
emit('complete', formModel)
modalApi.close()
}
const setFormData = async (data: any = null) => {
initData.value = cloneDeep(data)
loading.value = true
Object.assign(formData, initialFormData)
if (data) {
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
formData.channel = data.redio_key.split('_')[0]
formData.status = Number(formData.status)
}
loading.value = false
initData.value = cloneDeep(data)
loading.value = true
Object.assign(formModel, initialFormData)
if (data) {
Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] })
formModel.channel = data.redio_key.split('_')[0]
formModel.status = Number(formModel.status)
}
formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
}
const enableVerify = () => {
let verify = true
if ((formData.config.pay_explain_switch == 1 && Test.empty(formData.config.pay_explain_title)) || (formData.config.pay_explain_switch == 1 && Test.empty(formData.config.pay_explain_content)) || Test.empty(formData.config.pay_type_name) || Test.empty(formData.config.pay_page_name) || Test.empty(formData.config.pay_button_name) || Test.empty(formData.config.pay_leave_message)) verify = false
if ((formModel.config.pay_explain_switch == 1 && Test.empty(formModel.config.pay_explain_title)) || (formModel.config.pay_explain_switch == 1 && Test.empty(formModel.config.pay_explain_content)) || Test.empty(formModel.config.pay_type_name) || Test.empty(formModel.config.pay_page_name) || Test.empty(formModel.config.pay_button_name) || Test.empty(formModel.config.pay_leave_message)) verify = false
return verify
}
defineExpose({
showDialog,
setFormData,
enableVerify
})

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog v-model="showDialog" :title="t('updateOfflinepay')" width="550px" :destroy-on-close="true">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<Modal :class="'w-[550px]'" :title="t('updateOfflinepay')">
<BaseForm />
<el-form-item :label="t('collectionName')" prop="config.collection_name">
<el-input v-model.trim="formData.config.collection_name" :placeholder="t('collectionNamePlaceholder')" class="input-width" show-word-limit clearable />
@@ -17,23 +17,22 @@
<el-form-item :label="t('collectionDesc')" prop="config.collection_desc">
<el-input v-model.trim="formData.config.collection_desc" :placeholder="t('collectionDescPlaceholder')" class="input-width" type="textarea" rows="4" clearable />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
<el-button @click="cancel()">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -51,26 +50,24 @@ const initialFormData = {
status: 0,
is_default: 0
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
'config.collection_name': [
{ required: true, message: t('collectionNamePlaceholder'), trigger: 'blur' }
],
'config.collection_bank': [
{ required: true, message: t('collectionBankPlaceholder'), trigger: 'blur' }
],
'config.collection_account': [
{ required: true, message: t('collectionAccountPlaceholder'), trigger: 'blur' }
],
'config.collection_desc': [
{ required: true, message: t('collectionDescPlaceholder'), trigger: 'blur' }
]
}
const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
loading.value = true
const merged = { ...values }
emit('complete', merged)
loading.value = false
modalApi.close()
},
layout: 'horizontal',
schema: [
{ component: 'Input', fieldName: 'config.collection_name', label: t('collectionName'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'config.collection_bank', label: t('collectionBank'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'config.collection_account', label: t('collectionAccount'), rules: [{ required: true }] },
{ component: 'Textarea', fieldName: 'config.collection_desc', label: t('collectionDesc'), rules: [{ required: true }], componentProps: { rows: 4 } }
],
wrapperClass: 'grid-cols-1'
})
const emit = defineEmits(['complete'])
@@ -79,33 +76,24 @@ const emit = defineEmits(['complete'])
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
emit('complete', formData)
showDialog.value = false
}
})
}
const confirm = async () => { if (loading.value) return; formApi.submit() }
const setFormData = async (data: any = null) => {
loading.value = true
Object.assign(formData, initialFormData)
if (data) {
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
formData.channel = data.redio_key.split('_')[0]
formData.status = Number(formData.status)
}
loading.value = false
loading.value = true
Object.assign(formModel, initialFormData)
if (data) {
Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] })
formModel.channel = data.redio_key.split('_')[0]
formModel.status = Number(formModel.status)
}
formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog v-model="showDialog" :title="t('updateWechat')" width="500px" :destroy-on-close="true">
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<Modal :class="'w-[500px]'" :title="t('updateWechat')">
<BaseForm />
<el-form-item :label="t('mchId')" prop="config.mch_id">
<el-input v-model.trim="formData.config.mch_id" :placeholder="t('mchIdPlaceholder')" class="input-width" maxlength="32" show-word-limit clearable />
@@ -12,33 +12,33 @@
<div class="form-tip">{{ t('mchSecretKeyTips') }}</div>
</el-form-item>
<el-form-item :label="t('mchSecretCert')" prop="config.mch_secret_cert">
<el-form-item :label="t('mchSecretCert')">
<div class="input-width">
<upload-file v-model="formData.config.mch_secret_cert" api="sys/document/wechat" />
<upload-file v-model="formModel.config.mch_secret_cert" api="sys/document/wechat" />
</div>
<div class="form-tip">{{ t('mchSecretCertTips') }}</div>
</el-form-item>
<el-form-item :label="t('mchPublicCertPath')" prop="config.mch_public_cert_path">
<el-form-item :label="t('mchPublicCertPath')">
<div class="input-width">
<upload-file v-model="formData.config.mch_public_cert_path" api="sys/document/wechat" />
<upload-file v-model="formModel.config.mch_public_cert_path" api="sys/document/wechat" />
</div>
<div class="form-tip">{{ t('mchPublicCertPathTips') }}</div>
</el-form-item>
<el-form-item :label="t('wechatpayPublicCert')" prop="config.wechat_public_cert_path">
<el-form-item :label="t('wechatpayPublicCert')">
<div class="input-width">
<upload-file v-model="formData.config.wechat_public_cert_path" api="sys/document/wechat" />
<upload-file v-model="formModel.config.wechat_public_cert_path" api="sys/document/wechat" />
</div>
</el-form-item>
<el-form-item :label="t('wechatpayPublicCertId')" prop="config.wechat_public_cert_id">
<el-form-item :label="t('wechatpayPublicCertId')">
<div class="input-width">
<el-input v-model.trim="formData.config.wechat_public_cert_id" placeholder="" class="input-width" show-word-limit clearable />
<el-input v-model.trim="formModel.config.wechat_public_cert_id" placeholder="" class="input-width" show-word-limit clearable />
</div>
</el-form-item>
<el-form-item :label="t('jsapiDir')" v-show="formData.channel == 'wechat' || formData.channel == 'weapp'">
<el-form-item :label="t('jsapiDir')" v-show="formModel.channel == 'wechat' || formModel.channel == 'weapp'">
<el-input :model-value="wapDomain + '/'" placeholder="Please input" class="input-width" :readonly="true" :disabled="true">
<template #append>
<div class="cursor-pointer" @click="copyEvent(wapDomain + '/')">{{ t('copy') }}
@@ -48,7 +48,7 @@
<div class="form-tip !leading-normal">{{ t('jsapiDirTips') }}</div>
</el-form-item>
<el-form-item :label="t('h5Domain')" v-show="formData.channel == 'h5'">
<el-form-item :label="t('h5Domain')" v-show="formModel.channel == 'h5'">
<el-input :model-value="wapDomain.replace('http://', '').replace('https://', '')" placeholder="Please input" class="input-width" :readonly="true" :disabled="true">
<template #append>
<div class="cursor-pointer" @click="copyEvent(wapDomain.replace('http://', '').replace('https://', ''))">{{ t('copy') }}
@@ -58,7 +58,7 @@
<div class="form-tip !leading-normal">{{ t('h5DomainTips') }}</div>
</el-form-item>
<el-form-item :label="t('nativeDomain')" v-show="formData.channel == 'pc'">
<el-form-item :label="t('nativeDomain')" v-show="formModel.channel == 'pc'">
<el-input :model-value="serviceDomain" placeholder="Please input" class="input-width" :readonly="true" :disabled="true">
<template #append>
<div class="cursor-pointer" @click="copyEvent(serviceDomain)">{{ t('copy') }}
@@ -67,27 +67,28 @@
</el-input>
<div class="form-tip !leading-normal">{{ t('nativeDomainTips') }}</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { ref, reactive, watch } from 'vue'
import { t } from '@/lang'
import { FormInstance, ElMessage } from 'element-plus'
import { ElMessage } from 'element-plus'
import Test from '@/utils/test'
import { getUrl } from '@/app/api/sys'
import { useClipboard } from '@vueuse/core'
import { cloneDeep } from 'lodash-es'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
const wapDomain = ref('')
const serviceDomain = ref('')
@@ -115,26 +116,22 @@ const initialFormData = {
status: 0,
is_default: 0
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
'config.mch_id': [
{ required: true, message: t('mchIdPlaceholder'), trigger: 'blur' }
],
'config.mch_secret_key': [
{ required: true, message: t('mchSecretKeyPlaceholder'), trigger: 'blur' }
],
'config.mch_secret_cert': [
{ required: true, message: t('mchSecretCertPlaceholder'), trigger: 'blur' }
],
'config.mch_public_cert_path': [
{ required: true, message: t('mchPublicCertPathPlaceholder'), trigger: 'blur' }
]
}
const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
loading.value = true
const merged = { ...values, config: { ...formModel.config } }
emit('complete', merged)
loading.value = false
modalApi.close()
},
layout: 'horizontal',
schema: [
{ component: 'Input', fieldName: 'config.mch_id', label: t('mchId'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'config.mch_secret_key', label: t('mchSecretKey'), rules: [{ required: true }] }
],
wrapperClass: 'grid-cols-1'
})
const emit = defineEmits(['complete'])
@@ -143,45 +140,39 @@ const emit = defineEmits(['complete'])
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
emit('complete', formData)
showDialog.value = false
}
})
}
const confirm = async () => { if (loading.value) return; formApi.submit() }
const cancel = () => {
Object.assign(formData, initialFormData)
Object.assign(formModel, initialFormData)
if (initData.value) {
Object.keys(formData).forEach((key: string) => {
if (initData.value[key] != undefined) formData[key] = initData.value[key]
Object.keys(formModel).forEach((key: string) => {
if (initData.value[key] != undefined) formModel[key] = initData.value[key]
})
formData.channel = initData.value.redio_key.split('_')[0]
formData.status = Number(formData.status)
formModel.channel = initData.value.redio_key.split('_')[0]
formModel.status = Number(formModel.status)
}
emit('complete', formData)
showDialog.value = false
emit('complete', formModel)
modalApi.close()
}
const setFormData = async (data: any = null) => {
initData.value = cloneDeep(data)
loading.value = true
Object.assign(formData, initialFormData)
Object.assign(formModel, initialFormData)
if (data) {
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
Object.keys(formModel).forEach((key: string) => {
if (data[key] != undefined) formModel[key] = data[key]
})
formData.channel = data.redio_key.split('_')[0]
formData.status = Number(formData.status)
formModel.channel = data.redio_key.split('_')[0]
formModel.status = Number(formModel.status)
}
formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
}
const enableVerify = () => {
let verify = true
if (Test.empty(formData.config.mch_id) || Test.empty(formData.config.mch_secret_key) || Test.empty(formData.config.mch_secret_cert) || Test.empty(formData.config.mch_public_cert_path)) verify = false
if (Test.empty(formModel.config.mch_id) || Test.empty(formModel.config.mch_secret_key) || Test.empty(formModel.config.mch_secret_cert) || Test.empty(formModel.config.mch_public_cert_path)) verify = false
return verify
}
@@ -210,7 +201,6 @@ watch(copied, () => {
})
defineExpose({
showDialog,
setFormData,
enableVerify
})

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog v-model="showDialog" :title="t('aliSms')" width="580px" :destroy-on-close="true">
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<Modal :class="'w-[580px]'" :title="t('aliSms')">
<BaseForm />
<el-form-item :label="t('isUse')">
<el-radio-group v-model="formData.is_use">
<el-radio :label="1">{{ t('startUsing') }}</el-radio>
@@ -20,24 +20,23 @@
<el-input v-model.trim="formData.secret_key" :placeholder="t('aliSecretKeyPlaceholder')" class="input-width" clearable />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { getSmsInfo, editSms } from '@/app/api/notice'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -50,23 +49,21 @@ const initialFormData = {
secret_key: '',
is_use: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
sign: [
{ required: true, message: t('aliSignPlaceholder'), trigger: 'blur' }
],
app_key: [
{ required: true, message: t('aliAppKeyPlaceholder'), trigger: 'blur' }
],
secret_key: [
{ required: true, message: t('aliSecretKeyPlaceholder'), trigger: 'blur' }
]
}
const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
loading.value = true
editSms(values).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
},
layout: 'horizontal',
schema: [
{ component: 'RadioGroup', fieldName: 'is_use', label: t('isUse'), componentProps: { options: [ { label: t('startUsing'), value: 1 }, { label: t('statusDeactivate'), value: 0 } ] } },
{ component: 'Input', fieldName: 'sign', label: t('aliSign'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'app_key', label: t('aliAppKey'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'secret_key', label: t('aliSecretKey'), rules: [{ required: true }] }
],
wrapperClass: 'grid-cols-1'
})
const emit = defineEmits(['complete'])
@@ -75,44 +72,26 @@ const emit = defineEmits(['complete'])
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
editSms(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
// showDialog.value = false
})
}
})
}
const confirm = async () => { if (loading.value) return; formApi.submit() }
const setFormData = async (row: any = null) => {
loading.value = true
Object.assign(formData, initialFormData)
if (row) {
const data = await (await getSmsInfo(row.sms_type)).data
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
if (data.params[key] != undefined) formData[key] = data.params[key].value
})
}
loading.value = false
loading.value = true
Object.assign(formModel, initialFormData)
if (row) {
const data = await (await getSmsInfo(row.sms_type)).data
Object.keys(formModel).forEach((key: string) => {
if (data[key] != undefined) formModel[key] = data[key]
if (data.params && data.params[key] != undefined) formModel[key] = data.params[key].value
})
}
formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,51 +1,38 @@
<template>
<el-dialog v-model="showDialog" :title="t('messageInfo')" width="550px" :destroy-on-close="true">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('messageKey')">
<div class="input-width"> {{ formData.name }} </div>
</el-form-item>
<el-form-item :label="t('smsType')">
<div class="input-width"> {{ formData.sms_type_name }} </div>
</el-form-item>
<!-- <el-form-item :label="t('messageData')">
<div class="input-width"> {{ formData.message_data }} </div>
</el-form-item> -->
<!-- <el-form-item :label="t('nickname')">
<div class="input-width"> {{ formData.nickname }} </div>
</el-form-item> -->
<el-form-item :label="t('receiver')">
<div class="input-width"> {{ formData.mobile }} </div>
</el-form-item>
<el-form-item :label="t('createTime')">
<div class="input-width"> {{ formData.create_time }} </div>
</el-form-item>
<el-form-item :label="t('发送结果')">
<div class="input-width" v-if="formData.status == 'sending'"> 发送失败 </div>
<div class="input-width" v-if="formData.status == 'success'"> 发送成功 </div>
<div class="input-width" v-if="formData.status == 'fail'"> {{ formData.result }} </div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
<Modal :class="'w-[550px]'" :title="t('messageInfo')">
<div class="page-form" v-loading="loading">
<el-form-item :label="t('messageKey')">
<div class="input-width"> {{ formModel.name }} </div>
</el-form-item>
<el-form-item :label="t('smsType')">
<div class="input-width"> {{ formModel.sms_type_name }} </div>
</el-form-item>
<el-form-item :label="t('receiver')">
<div class="input-width"> {{ formModel.mobile }} </div>
</el-form-item>
<el-form-item :label="t('createTime')">
<div class="input-width"> {{ formModel.create_time }} </div>
</el-form-item>
<el-form-item :label="t('发送结果')">
<div class="input-width" v-if="formModel.status == 'sending'"> 发送失败 </div>
<div class="input-width" v-if="formModel.status == 'success'"> 发送成功 </div>
<div class="input-width" v-if="formModel.status == 'fail'"> {{ formModel.result }} </div>
</el-form-item>
</div>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="close()">{{ t('confirm') }}</el-button>
</span>
</template>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -63,34 +50,21 @@ const initialFormData = {
status:'',
result:''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
}
})
const formModel: Record<string, any> = reactive({ ...initialFormData })
const setFormData = async (row: any = null) => {
loading.value = true
Object.assign(formData, initialFormData)
if (row) {
Object.keys(formData).forEach((key: string) => {
if (row[key] != undefined) formData[key] = row[key]
})
}
loading.value = false
loading.value = true
Object.assign(formModel, initialFormData)
if (row) {
Object.keys(formModel).forEach((key: string) => { if (row[key] != undefined) formModel[key] = row[key] })
}
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const close = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,52 +1,25 @@
<template>
<el-dialog v-model="showDialog" :title="t('tencentSms')" width="580px" :destroy-on-close="true">
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('isUse')">
<el-radio-group v-model="formData.is_use">
<el-radio :label="1">{{ t('startUsing') }}</el-radio>
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('tencentSign')" prop="sign">
<el-input v-model.trim="formData.sign" :placeholder="t('tencentSignPlaceholder')" class="input-width" show-word-limit clearable />
</el-form-item>
<el-form-item :label="t('tencentAppId')" prop="app_id">
<el-input v-model.trim="formData.app_id" :placeholder="t('tencentAppIdPlaceholder')" class="input-width" show-word-limit clearable />
</el-form-item>
<el-form-item :label="t('tencentSecretId')" prop="secret_id">
<el-input v-model.trim="formData.secret_id" :placeholder="t('tencentSecretIdPlaceholder')" class="input-width" clearable />
</el-form-item>
<el-form-item :label="t('tencentSecretKey')" prop="secret_key">
<el-input v-model.trim="formData.secret_key" :placeholder="t('tencentSecretKeyPlaceholder')" class="input-width" clearable />
</el-form-item>
</el-form>
<Modal :class="'w-[580px]'" :title="t('tencentSms')">
<BaseForm />
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</template>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { getSmsInfo, editSms } from '@/app/api/notice'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
/**
* 表单数据
*/
const initialFormData = {
sms_type: '',
sign: '',
@@ -56,72 +29,44 @@ const initialFormData = {
app_id: '',
secret_id: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formModel: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
sign: [
{ required: true, message: t('tencentSignPlaceholder'), trigger: 'blur' }
],
app_id: [
{ required: true, message: t('tencentAppIdPlaceholder'), trigger: 'blur' }
],
secret_id: [
{ required: true, message: t('tencentSecretIdPlaceholder'), trigger: 'blur' }
],
secret_key: [
{ required: true, message: t('tencentSecretKeyPlaceholder'), trigger: 'blur' }
]
}
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
loading.value = true
editSms(values).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
},
layout: 'horizontal',
schema: [
{ component: 'RadioGroup', fieldName: 'is_use', label: t('isUse'), componentProps: { options: [ { label: t('startUsing'), value: 1 }, { label: t('statusDeactivate'), value: 0 } ] } },
{ component: 'Input', fieldName: 'sign', label: t('tencentSign'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'app_id', label: t('tencentAppId'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'secret_id', label: t('tencentSecretId'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'secret_key', label: t('tencentSecretKey'), rules: [{ required: true }] }
],
wrapperClass: 'grid-cols-1'
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
editSms(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
// showDialog.value = false
})
}
})
}
const setFormData = async (row: any = null) => {
loading.value = true
Object.assign(formData, initialFormData)
if (row) {
const data = await (await getSmsInfo(row.sms_type)).data
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
if (data.params[key] != undefined) formData[key] = data.params[key].value
})
}
loading.value = false
loading.value = true
Object.assign(formModel, initialFormData)
if (row) {
const data = await (await getSmsInfo(row.sms_type)).data
Object.keys(formModel).forEach((key: string) => {
if (data[key] != undefined) formModel[key] = data[key]
if (data.params && data.params[key] != undefined) formModel[key] = data.params[key].value
})
}
formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const emit = defineEmits(['complete'])
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -138,6 +138,12 @@
</el-form>
</div>
<ModalPassword :class="'w-[400px]'" :title="'请保存好新密码'">
<div class="p-2"> 新密码为{{ newPassword }} </div>
<template #footer>
<el-button type="primary" @click="passwordConfirm">{{ t('confirm') }}</el-button>
</template>
</ModalPassword>
</el-card>
</template>
@@ -145,6 +151,7 @@
import { ref, computed, reactive } from 'vue'
import { loginAccount, getSmsCaptcha, getSmsSend, resetPassword, registerAccount, getSmsSignConfig } from '@/app/api/notice'
import { t } from '@/lang'
import { useVbenModal } from '@vben/common-ui'
const props = defineProps({
info: {
@@ -501,22 +508,17 @@ const reset = async () => {
mobile: changeFormData.value.mobile
}
resetPassword(changeFormData.value.username, { ...params }).then((res) => {
const newPassword = res.data.password
ElMessageBox.confirm(`新密码为:${newPassword}`, '请保存好新密码', {
confirmButtonText: '确定',
showCancelButton: false
}).then(() => {
type.value = 'login'
emit('complete')
}).catch(() => {
type.value = 'login'
emit('complete')
})
newPassword.value = res.data.password
modalPasswordApi.open()
})
}
})
}
const [ModalPassword, modalPasswordApi] = useVbenModal()
const newPassword = ref('')
const passwordConfirm = () => { modalPasswordApi.close(); type.value = 'login'; emit('complete') }
</script>
<style lang="scss" scoped>

View File

@@ -26,40 +26,42 @@
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit"
layout="total, sizes, prev, pager, next, jumper" :total="tableData.total" @size-change="loadRankList()" @current-change="loadRankList" />
</div>
<el-dialog v-model="visibleDetail" :title="t('模版详情')" width="600px" destroy-on-close >
<el-form label-width="100px" ref="formRef" class="page-form" v-loading="loading">
<el-form-item :label="t('订单编号')" prop="template_id">
<div>{{ detail.order_no }}</div>
</el-form-item>
<el-form-item :label="t('用户名称')" prop="template_id">
<div>{{ detail.username }}</div>
</el-form-item>
<el-form-item :label="t('套餐名称')" prop="template_id">
<div>{{ detail.package_name }}</div>
</el-form-item>
<el-form-item :label="t('订单状态')" prop="title">
<div >{{ detail.order_status_name }}</div>
</el-form-item>
<el-form-item :label="t('短信条数')" prop="title">
<div >{{ detail.sms_num }}</div>
</el-form-item>
<el-form-item :label="t('订单金额')" prop="title">
<div >{{ detail.order_money }}</div>
</el-form-item>
<el-form-item :label="t('付款金额')" prop="title">
<div >{{ detail.pay_money }}</div>
</el-form-item>
<el-form-item :label="t('创建时间')" prop="title">
<div >{{ detail.create_time }}</div>
</el-form-item>
</el-form>
<ModalDetail :class="'w-[600px]'" :title="t('模版详情')">
<div v-loading="loading">
<el-form label-width="100px" ref="formRef" class="page-form">
<el-form-item :label="t('订单编号')" prop="template_id">
<div>{{ detail.order_no }}</div>
</el-form-item>
<el-form-item :label="t('用户名称')" prop="template_id">
<div>{{ detail.username }}</div>
</el-form-item>
<el-form-item :label="t('套餐名称')" prop="template_id">
<div>{{ detail.package_name }}</div>
</el-form-item>
<el-form-item :label="t('订单状态')" prop="title">
<div>{{ detail.order_status_name }}</div>
</el-form-item>
<el-form-item :label="t('短信条数')" prop="title">
<div>{{ detail.sms_num }}</div>
</el-form-item>
<el-form-item :label="t('订单金额')" prop="title">
<div>{{ detail.order_money }}</div>
</el-form-item>
<el-form-item :label="t('付款金额')" prop="title">
<div>{{ detail.pay_money }}</div>
</el-form-item>
<el-form-item :label="t('创建时间')" prop="title">
<div>{{ detail.create_time }}</div>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visibleDetail = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="visibleDetail = false">{{ t("confirm") }}</el-button>
<el-button @click="detailCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="detailConfirm">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalDetail>
</div>
</template>
@@ -67,6 +69,7 @@
import { ref, reactive, onMounted } from 'vue'
import { getSmsOrdersList, getOrderInfo } from '@/app/api/notice'
import { t } from '@/lang'
import { useVbenModal } from '@vben/common-ui'
const props = defineProps({
username: {
@@ -106,18 +109,19 @@ const loadRankList = () => {
}
// 详情
const [ModalDetail, modalDetailApi] = useVbenModal()
const detail = ref({})
const visibleDetail = ref(false)
const loading = ref(false)
const detailEvent = (row:any) => {
loading.value = true
visibleDetail.value = true
modalDetailApi.open()
getOrderInfo(props.username, { out_trade_no: row.out_trade_no }).then(res => {
detail.value = res.data
loading.value = false
})
visibleDetail.value = true
}
const detailCancel = () => { modalDetailApi.close() }
const detailConfirm = () => { modalDetailApi.close() }
onMounted(() => {
if (props.username) {
loadRankList()

View File

@@ -81,71 +81,32 @@
<el-button @click="visible = false">{{ t("cancel") }}</el-button>
</template>
</el-dialog>
<el-dialog v-model="visibleAdd" :title="t('添加签名')" width="800px" destroy-on-close :close-on-click-modal="false">
<el-form label-width="150px" :model="formData" ref="formRef" :rules="formRules" class="page-form ml-[20px]">
<el-form-item :label="t('短信签名')" prop="signature">
<el-input v-model="formData.signature" placeholder="请输入短信签名" class="input-width" maxlength="20" show-word-limit clearable />
</el-form-item>
<div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">必须由包裹例如test</div>
<div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">字数要求在2-20个字符不能使用空格和特殊符号 - + = * & % # @ ~;</div>
<el-form-item :label="t('短信示例内容')" prop="contentExample">
<el-input v-model="formData.contentExample" placeholder="请输入短信示例内容" clearable maxlength="50" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('企业名称')" prop="companyName">
<el-input v-model="formData.companyName" placeholder="请输入企业名称" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('社会统一信用代码')" prop="creditCode">
<el-input v-model="formData.creditCode" placeholder="请输入社会统一信用代码" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('法人姓名')" prop="legalPerson">
<el-input v-model="formData.legalPerson" placeholder="请输入法人姓名" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('经办人姓名')" prop="principalName">
<el-input v-model="formData.principalName" placeholder="请输入经办人姓名" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('经办人手机号')" prop="principalMobile">
<el-input v-model="formData.principalMobile" placeholder="请输入经办人手机号" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('经办人身份证')" prop="principalIdCard">
<el-input v-model="formData.principalIdCard" placeholder="请输入经办人身份证" clearable maxlength="18" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('签名来源')">
<el-radio-group v-model="formData.signSource" >
<el-radio v-for="item in signConfig.signSourceList" :key="item.type" :label="item.type" >{{item.name}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('签名类型')">
<el-radio-group v-model="formData.signType">
<el-radio v-for="item in signConfig.signTypeList" :key="item.type" :label="item.type" >{{item.name}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('上传图片')" prop="imgUrl">
<upload-image v-model="formData.imgUrl" :limit="1" />
</el-form-item>
<div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">当签名来源为商标APP小程序事业单位简称或企业名称简称时需必填此字段</div>
<div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">当签名来源为事业单位全称或企业名称全称时选填此字段</div>
<el-form-item :label="t('是否默认')">
<el-radio-group v-model="formData.defaultSign" >
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<Modal :class="'w-[800px]'" :title="t('添加签名')">
<BaseForm />
<div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">必须由包裹例如test</div>
<div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">字数要求在2-20个字符不能使用空格和特殊符号 - + = * & % # @ ~;</div>
<el-form-item :label="t('上传图片')">
<upload-image v-model="formData.imgUrl" :limit="1" />
</el-form-item>
<div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">当签名来源为商标APP小程序事业单位简称或企业名称简称时需必填此字段</div>
<div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">当签名来源为事业单位全称或企业名称全称时选填此字段</div>
<template #footer>
<el-button @click="visibleAdd = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="onSave()">{{ t("confirm") }}</el-button>
<el-button @click="modalApi.close()">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="submitVben()">{{ t("confirm") }}</el-button>
</template>
</el-dialog>
</Modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, reactive } from 'vue'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
import { getSignList, addSign, getSmsSignConfig, deleteSign } from '@/app/api/notice'
import { t } from '@/lang'
const visible = ref(false)
const visibleAdd = ref(false)
const [Modal, modalApi] = useVbenModal()
const emit = defineEmits(['select'])
const props = defineProps({
username: {
@@ -185,6 +146,28 @@ const getSmsSignConfigFn = () => {
getSmsSignConfigFn()
const formRef = ref()
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
const merged = { ...values, imgUrl: formData.imgUrl, defaultSign: formData.defaultSign, signSource: formData.signSource, signType: formData.signType }
addSign(props.username, merged).then(() => { setTimeout(() => { modalApi.close(); loadSignList() }, 500) })
},
layout: 'horizontal',
schema: [
{ component: 'Input', fieldName: 'signature', label: t('短信签名'), rules: [{ required: true }, { validator: (v) => /^[^]*$/.test(v) ? true : '短信签名必须被【】包裹' }] },
{ component: 'Input', fieldName: 'contentExample', label: t('短信示例内容'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'companyName', label: t('企业名称'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'creditCode', label: t('社会统一信用代码'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'legalPerson', label: t('法人姓名'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'principalName', label: t('经办人姓名'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'principalMobile', label: t('经办人手机号'), rules: [{ required: true }, { validator: (v) => /^1[3-9]\d{9}$/.test(v) ? true : t('请输入正确的手机号码') }] },
{ component: 'Input', fieldName: 'principalIdCard', label: t('经办人身份证'), rules: [{ required: true }, { validator: (v) => /^[1-9]\d{5}(19|20)\d{2}((0\d)|(1[0-2]))(([0-2]\d)|3[0-1])\d{3}([0-9Xx])$/.test(v) ? true : t('请输入正确的身份证号码') }] },
{ component: 'RadioGroup', fieldName: 'signSource', label: t('签名来源'), componentProps: { options: () => signConfig.signSourceList.map((i:any) => ({ label: i.name, value: i.type })) } },
{ component: 'RadioGroup', fieldName: 'signType', label: t('签名类型'), componentProps: { options: () => signConfig.signTypeList.map((i:any) => ({ label: i.name, value: i.type })) } },
{ component: 'RadioGroup', fieldName: 'defaultSign', label: t('是否默认'), componentProps: { options: [ { label: '是', value: 1 }, { label: '否', value: 0 } ] } }
],
wrapperClass: 'grid-cols-1'
})
const formRules = computed(() => {
return {
signature: [
@@ -273,18 +256,7 @@ const phoneVerify = (rule: any, value: any, callback: any) => {
}
}
const onSave = async () => {
await formRef.value?.validate(async (valid) => {
if (valid) {
addSign(props.username, formData).then((res) => {
setTimeout(() => {
visibleAdd.value = false
loadSignList()
}, 500)
})
}
})
}
const submitVben = () => { formApi.submit() }
// 表单内容
const tableData = reactive({
@@ -321,7 +293,8 @@ const addEvent = () => {
Object.assign(formData, initialFormData)
formData.signSource = signConfig.signSourceList[0].type
formData.signType = signConfig.signTypeList[0].type
visibleAdd.value = true
formApi.setModel({ ...formData })
modalApi.open()
}
const deleteTemplate = (row:any) => {

View File

@@ -54,67 +54,66 @@
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit"
layout="total, sizes, prev, pager, next, jumper" :total="tableData.total"/>
</div>
<el-dialog v-model="visibleDetail" :title="t('模版详情')" width="600px" destroy-on-close >
<ModalDetail :class="'w-[600px]'" :title="t('模版详情')">
<el-form label-width="100px" ref="formRef" class="page-form">
<el-form-item :label="t('短信类型')" prop="template_id">
<div>{{ detail.sms_type }}</div>
<div>{{ detail?.sms_type }}</div>
</el-form-item>
<el-form-item :label="t('模版名称')" prop="template_id">
<div>{{ detail.name }}</div>
<div>{{ detail?.name }}</div>
</el-form-item>
<el-form-item :label="t('模版类型')" prop="title">
<div >{{ detail.title }}</div>
<div>{{ detail?.title }}</div>
</el-form-item>
<el-form-item :label="t('短信内容')" prop="title" v-if="detail.sms">
<div >{{ detail.sms?.content }}</div>
<el-form-item :label="t('短信内容')" prop="title" v-if="detail?.sms">
<div>{{ detail?.sms?.content }}</div>
</el-form-item>
<el-form-item :label="t('审核状态')" prop="title">
<div >{{ detail.audit_info.audit_status_name }}</div>
<div>{{ detail?.audit_info?.audit_status_name }}</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<!-- <el-button @click="visibleDetail = false">{{ t("cancel") }}</el-button> -->
<el-button type="primary" @click="visibleDetail = false">{{ t("confirm") }}</el-button>
<el-button type="primary" @click="detailConfirm">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="visibleReport" :title="t('模版报备')" width="820px" destroy-on-close >
<el-form label-width="100px" ref="formRef" class="page-form" v-loading="reportLoading">
<el-form-item :label="t('模版名称')" prop="template_id">
<div class="input-width">{{ detail.name }}</div>
</el-form-item>
<el-form-item :label="t('模版类型')" prop="title">
<el-radio-group v-model="reportData.template_type">
<el-radio v-for="[key, value] in Object.entries(template_type_list)" :key="key" :label="Number(key)">{{ value }}</el-radio>
</el-radio-group>
</el-form-item>
<div class="ml-[100px] mb-[10px] mt-[-10px] text-[12px] text-[#999] leading-[20px]">
<div>验证码仅支持验证码类型变量</div>
<div>行业通知支持验证码类型变量</div>
<div>营销推广不支持变量</div>
</div>
<el-form-item :label="t('变量类型')" prop="params_json" v-if="detail.variable && Object.keys(detail.variable).length > 0">
<div v-for="(label, key) in detail.variable" :key="key" class="mb-2 flex items-center">
<div class="flex flex-1 items-center">
<div class="w-32 mr-1 ">{{ label }}</div>
<el-select v-model="reportData.params_json[key]" placeholder="请选择类型" class="flex-1" filterable clearable :disabled="isMarketingWithVariable">
<el-option v-for="item in filteredParamTypes" :key="item.type" :label="item.name + '' + item.desc + ''" :value="item.type"/>
</el-select>
</div>
</ModalDetail>
<ModalReport :class="'w-[820px]'" :title="t('模版报备')">
<div v-loading="reportLoading">
<el-form label-width="100px" ref="formRef" class="page-form">
<el-form-item :label="t('模版名称')" prop="template_id">
<div class="input-width">{{ detail?.name }}</div>
</el-form-item>
<el-form-item :label="t('模版类型')" prop="title">
<el-radio-group v-model="reportData.template_type">
<el-radio v-for="[key, value] in Object.entries(template_type_list)" :key="key" :label="Number(key)">{{ value }}</el-radio>
</el-radio-group>
</el-form-item>
<div class="ml-[100px] mb-[10px] mt-[-10px] text-[12px] text-[#999] leading-[20px]">
<div>验证码支持验证码类型变量</div>
<div>行业通知不支持验证码类型变量</div>
<div>营销推广不支持变量</div>
</div>
</el-form-item>
</el-form>
<el-form-item :label="t('变量类型')" prop="params_json" v-if="detail?.variable && Object.keys(detail.variable).length > 0">
<div v-for="(label, key) in detail.variable" :key="key" class="mb-2 flex items-center">
<div class="flex flex-1 items-center">
<div class="w-32 mr-1 ">{{ label }}</div>
<el-select v-model="reportData.params_json[key]" placeholder="请选择类型" class="flex-1" filterable clearable :disabled="isMarketingWithVariable">
<el-option v-for="item in filteredParamTypes" :key="item.type" :label="item.name + '' + item.desc + ''" :value="item.type"/>
</el-select>
</div>
</div>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visibleReport = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="reportTemplateFn()" :disabled="isMarketingWithVariable">{{ t("confirm") }}</el-button>
<el-button @click="reportCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="reportConfirm" :disabled="isMarketingWithVariable">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="visibleAsync" :title="t('同步模版状态')" width="800px" destroy-on-close >
</ModalReport>
<ModalAsync :class="'w-[800px]'" :title="t('同步模版状态')">
<el-alert type="warning" :closable="false" class="!mb-[10px]">
<template #default>
以下模版名称重复请先调整模版名称后重新同步模版
@@ -125,20 +124,20 @@
<el-table :data="repeatListArray" border style="width: 100%;">
<el-table-column label="模版名称" prop="name" />
<el-table-column label="插件名称">
<template #default="{ row }">
<el-tag v-for="item in row.platforms" :key="item" class="mr-1 mb-1">{{ item }}</el-tag>
</template>
<template #default="{ row }">
<el-tag v-for="item in row.platforms" :key="item" class="mr-1 mb-1">{{ item }}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visibleAsync = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="visibleAsync = false">{{ t("confirm") }}</el-button>
<el-button @click="asyncCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="asyncConfirm">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</ModalAsync>
</div>
</template>
@@ -146,6 +145,7 @@
import { ref, computed, reactive, onMounted, watch } from 'vue'
import { getTemplateList, getTemplateReportConfig, reportTemplate, templateSync, getreportTemplateInfo, clearTemplate } from '@/app/api/notice'
import { t } from '@/lang'
import { useVbenModal } from '@vben/common-ui'
const props = defineProps({
username: {
@@ -222,13 +222,15 @@ onMounted(() => {
}
})
const visibleAsync = ref(false)
const [ModalDetail, modalDetailApi] = useVbenModal()
const [ModalReport, modalReportApi] = useVbenModal()
const [ModalAsync, modalAsyncApi] = useVbenModal()
const repeatList = ref({})
const syncEvent = () => {
templateSync('niuyun', props.username).then((res) => {
repeatList.value = res.data.repeat_list
if (repeatList.value && Object.keys(repeatList.value).length > 0) {
visibleAsync.value = true
modalAsyncApi.open()
} else {
loadSmsTemplateList()
}
@@ -244,10 +246,9 @@ const repeatListArray = computed(() => {
// 详情
const detail = ref(null)
const visibleDetail = ref(false)
const editEvent = (row:any) => {
visibleDetail.value = true
detail.value = row
modalDetailApi.open()
}
// 清除报备
@@ -267,7 +268,6 @@ const clearEvent = (row:any) => {
const template_params_type_list = ref({})
const template_type_list = ref({})
const template_status_list = ref({})
const visibleReport = ref(false)
const reportData = ref({
template_type: 1,
template_key: '',
@@ -307,9 +307,9 @@ const reportEvent = (row:any) => {
if (!signature) {
ElMessage.error('请先配置签名')
} else {
detail.value = row
modalReportApi.open()
if (row.template_id) {
visibleReport.value = true
detail.value = row
getreportTemplateInfo('niuyun', props.username, { template_key: row.key }).then((res) => {
const paramJson = res.data?.param_json ?? {}
reportData.value.template_key = res.data.template_key
@@ -323,9 +323,7 @@ const reportEvent = (row:any) => {
reportLoading.value = false
})
} else {
visibleReport.value = true
reportLoading.value = false
detail.value = row
reportData.value.template_type = 1
reportData.value.template_key = detail.value.key
reportData.value.params_json = {}
@@ -350,10 +348,16 @@ const reportTemplateFn = () => {
reportData.value.template_id = Number(detail.value.template_id)
}
reportTemplate(detail.value.sms_type, props.username, reportData.value).then((res) => {
visibleReport.value = false
modalReportApi.close()
loadSmsTemplateList()
})
}
const detailConfirm = () => { modalDetailApi.close() }
const reportCancel = () => { modalReportApi.close() }
const reportConfirm = () => { reportTemplateFn() }
const asyncCancel = () => { modalAsyncApi.close() }
const asyncConfirm = () => { modalAsyncApi.close() }
</script>
<style lang="scss" scoped>

View File

@@ -1,53 +1,24 @@
<template>
<el-dialog v-model="showDialog" :title="t('aliStorage')" width="580px" :destroy-on-close="true">
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('isUse')">
<el-radio-group v-model="formData.is_use">
<el-radio label="1">{{ t('startUsing') }}</el-radio>
<el-radio label="0">{{ t('statusDeactivate') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('aliBucket')" prop="bucket">
<el-input v-model.trim="formData.bucket" :placeholder="t('aliBucketPlaceholder')" class="input-width" show-word-limit clearable />
<div class="form-tip">{{ t('aliBucketTips') }}</div>
</el-form-item>
<el-form-item :label="t('aliAccessKey')" prop="access_key">
<el-input v-model.trim="formData.access_key" :placeholder="t('aliAccessKeyPlaceholder')" class="input-width" clearable />
</el-form-item>
<el-form-item :label="t('aliSecretKey')" prop="secret_key">
<el-input v-model.trim="formData.secret_key" :placeholder="t('aliSecretKeyPlaceholder')" class="input-width" clearable />
</el-form-item>
<el-form-item :label="t('aliEndpoint')" prop="endpoint">
<el-input v-model.trim="formData.endpoint" :placeholder="t('aliEndpointPlaceholder')" class="input-width" clearable />
<div class="form-tip">{{ t('aliEndpointTips') }}</div>
</el-form-item>
<el-form-item :label="t('domain')" prop="domain">
<el-input v-model.trim="formData.domain" :placeholder="t('domainPlaceholder')" class="input-width" clearable />
</el-form-item>
</el-form>
<Modal :class="'w-[580px]'" :title="t('aliStorage')">
<BaseForm />
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { getStorageInfo, editStorage } from '@/app/api/sys'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -62,29 +33,24 @@ const initialFormData = {
domain: '',
is_use: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
bucket: [
{ required: true, message: t('aliBucketPlaceholder'), trigger: 'blur' }
],
access_key: [
{ required: true, message: t('aliAccessKeyPlaceholder'), trigger: 'blur' }
],
secret_key: [
{ required: true, message: t('aliSecretKeyPlaceholder'), trigger: 'blur' }
],
endpoint: [
{ required: true, message: t('aliEndpointPlaceholder'), trigger: 'blur' }
],
domain: [
{ required: true, message: t('domainPlaceholder'), trigger: 'blur' }
]
}
const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
loading.value = true
const data = { ...values }
editStorage(data).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
},
layout: 'horizontal',
schema: [
{ component: 'RadioGroup', fieldName: 'is_use', label: t('isUse'), componentProps: { options: [ { label: t('startUsing'), value: '1' }, { label: t('statusDeactivate'), value: '0' } ] } },
{ component: 'Input', fieldName: 'bucket', label: t('aliBucket'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'access_key', label: t('aliAccessKey'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'secret_key', label: t('aliSecretKey'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'endpoint', label: t('aliEndpoint'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'domain', label: t('domain'), rules: [{ required: true }] }
],
wrapperClass: 'grid-cols-1'
})
const emit = defineEmits(['complete'])
@@ -93,43 +59,26 @@ const emit = defineEmits(['complete'])
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
editStorage(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
})
}
})
}
const confirm = async () => { if (loading.value) return; formApi.submit() }
const setFormData = async (row: any = null) => {
loading.value = true
Object.assign(formData, initialFormData)
if (row) {
const data = await (await getStorageInfo(row.storage_type)).data
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
if (data.params[key] != undefined) formData[key] = data.params[key].value
})
}
loading.value = false
loading.value = true
Object.assign(formModel, initialFormData)
if (row) {
const data = await (await getStorageInfo(row.storage_type)).data
Object.keys(formModel).forEach((key: string) => {
if (data[key] != undefined) formModel[key] = data[key]
if (data.params && data.params[key] != undefined) formModel[key] = data.params[key].value
})
}
formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

View File

@@ -1,31 +1,24 @@
<template>
<el-dialog v-model="showDialog" :title="t('localStorage')" width="580px" :destroy-on-close="true">
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('isUse')">
<el-radio-group v-model="formData.is_use">
<el-radio label="1">{{ t('startUsing') }}</el-radio>
<el-radio label="0">{{ t('statusDeactivate') }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<Modal :class="'w-[580px]'" :title="t('localStorage')">
<BaseForm />
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
<el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</Modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { getStorageInfo, editStorage } from '@/app/api/sys'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const [Modal, modalApi] = useVbenModal()
const loading = ref(true)
/**
@@ -36,15 +29,18 @@ const initialFormData = {
storage_type: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
}
const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
loading.value = true
editStorage(values).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
},
layout: 'horizontal',
schema: [
{ component: 'RadioGroup', fieldName: 'is_use', label: t('isUse'), componentProps: { options: [ { label: t('startUsing'), value: '1' }, { label: t('statusDeactivate'), value: '0' } ] } }
],
wrapperClass: 'grid-cols-1'
})
const emit = defineEmits(['complete'])
@@ -53,42 +49,23 @@ const emit = defineEmits(['complete'])
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
editStorage(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
})
}
})
}
const confirm = async () => { if (loading.value) return; formApi.submit() }
const setFormData = async (row: any = null) => {
loading.value = true
Object.assign(formData, initialFormData)
if (row) {
const data = await (await getStorageInfo(row.storage_type)).data
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
}
loading.value = false
loading.value = true
Object.assign(formModel, initialFormData)
if (row) {
const data = await (await getStorageInfo(row.storage_type)).data
Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] })
}
formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
}
defineExpose({
showDialog,
setFormData
})
const cancel = () => { modalApi.close() }
defineExpose({ setFormData })
</script>
<style lang="scss" scoped></style>

Some files were not shown because too many files have changed in this diff Show More