Merge remote-tracking branch 'yudao/dev' into dev

This commit is contained in:
jason
2025-10-26 21:10:27 +08:00
171 changed files with 5261 additions and 1026 deletions

View File

@@ -6,6 +6,7 @@ import { requestClient } from '#/api/request';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
const accessStore = useAccessStore(); const accessStore = useAccessStore();
export namespace AiMindmapApi { export namespace AiMindmapApi {
// AI 思维导图 // AI 思维导图
export interface MindMap { export interface MindMap {
@@ -19,7 +20,7 @@ export namespace AiMindmapApi {
} }
// AI 思维导图生成 // AI 思维导图生成
export interface AiMindMapGenerateReq { export interface AiMindMapGenerateReqVO {
prompt: string; prompt: string;
} }
} }
@@ -32,7 +33,7 @@ export function generateMindMap({
ctrl, ctrl,
}: { }: {
ctrl: AbortController; ctrl: AbortController;
data: AiMindmapApi.AiMindMapGenerateReq; data: AiMindmapApi.AiMindMapGenerateReqVO;
onClose?: (...args: any[]) => void; onClose?: (...args: any[]) => void;
onError?: (...args: any[]) => void; onError?: (...args: any[]) => void;
onMessage?: (res: any) => void; onMessage?: (res: any) => void;
@@ -53,12 +54,12 @@ export function generateMindMap({
}); });
} }
// 查询思维导图分页 /** 查询思维导图分页 */
export function getMindMapPage(params: any) { export function getMindMapPage(params: any) {
return requestClient.get(`/ai/mind-map/page`, { params }); return requestClient.get(`/ai/mind-map/page`, { params });
} }
// 删除思维导图 /** 删除思维导图 */
export function deleteMindMap(id: number) { export function deleteMindMap(id: number) {
return requestClient.delete(`/ai/mind-map/delete?id=${id}`); return requestClient.delete(`/ai/mind-map/delete?id=${id}`);
} }

View File

@@ -13,7 +13,7 @@ export namespace AiModelApiKeyApi {
} }
} }
// 查询 API 密钥分页 /** 查询 API 密钥分页 */
export function getApiKeyPage(params: PageParam) { export function getApiKeyPage(params: PageParam) {
return requestClient.get<PageResult<AiModelApiKeyApi.ApiKey>>( return requestClient.get<PageResult<AiModelApiKeyApi.ApiKey>>(
'/ai/api-key/page', '/ai/api-key/page',
@@ -21,28 +21,29 @@ export function getApiKeyPage(params: PageParam) {
); );
} }
// 获得 API 密钥列表 /** 获得 API 密钥列表 */
export function getApiKeySimpleList() { export function getApiKeySimpleList() {
return requestClient.get<AiModelApiKeyApi.ApiKey[]>( return requestClient.get<AiModelApiKeyApi.ApiKey[]>(
'/ai/api-key/simple-list', '/ai/api-key/simple-list',
); );
} }
// 查询 API 密钥详情 /** 查询 API 密钥详情 */
export function getApiKey(id: number) { export function getApiKey(id: number) {
return requestClient.get<AiModelApiKeyApi.ApiKey>(`/ai/api-key/get?id=${id}`); return requestClient.get<AiModelApiKeyApi.ApiKey>(`/ai/api-key/get?id=${id}`);
} }
// 新增 API 密钥
/** 新增 API 密钥 */
export function createApiKey(data: AiModelApiKeyApi.ApiKey) { export function createApiKey(data: AiModelApiKeyApi.ApiKey) {
return requestClient.post('/ai/api-key/create', data); return requestClient.post('/ai/api-key/create', data);
} }
// 修改 API 密钥 /** 修改 API 密钥 */
export function updateApiKey(data: AiModelApiKeyApi.ApiKey) { export function updateApiKey(data: AiModelApiKeyApi.ApiKey) {
return requestClient.put('/ai/api-key/update', data); return requestClient.put('/ai/api-key/update', data);
} }
// 删除 API 密钥 /** 删除 API 密钥 */
export function deleteApiKey(id: number) { export function deleteApiKey(id: number) {
return requestClient.delete(`/ai/api-key/delete?id=${id}`); return requestClient.delete(`/ai/api-key/delete?id=${id}`);
} }

View File

@@ -20,7 +20,7 @@ export namespace AiModelChatRoleApi {
} }
// AI 聊天角色 分页请求 // AI 聊天角色 分页请求
export interface ChatRolePageReq { export interface ChatRolePageReqVO {
name?: string; // 角色名称 name?: string; // 角色名称
category?: string; // 角色类别 category?: string; // 角色类别
publicStatus: boolean; // 是否公开 publicStatus: boolean; // 是否公开
@@ -29,7 +29,7 @@ export namespace AiModelChatRoleApi {
} }
} }
// 查询聊天角色分页 /** 查询聊天角色分页 */
export function getChatRolePage(params: PageParam) { export function getChatRolePage(params: PageParam) {
return requestClient.get<PageResult<AiModelChatRoleApi.ChatRole>>( return requestClient.get<PageResult<AiModelChatRoleApi.ChatRole>>(
'/ai/chat-role/page', '/ai/chat-role/page',
@@ -37,49 +37,49 @@ export function getChatRolePage(params: PageParam) {
); );
} }
// 查询聊天角色详情 /** 查询聊天角色详情 */
export function getChatRole(id: number) { export function getChatRole(id: number) {
return requestClient.get<AiModelChatRoleApi.ChatRole>( return requestClient.get<AiModelChatRoleApi.ChatRole>(
`/ai/chat-role/get?id=${id}`, `/ai/chat-role/get?id=${id}`,
); );
} }
// 新增聊天角色
/** 新增聊天角色 */
export function createChatRole(data: AiModelChatRoleApi.ChatRole) { export function createChatRole(data: AiModelChatRoleApi.ChatRole) {
return requestClient.post('/ai/chat-role/create', data); return requestClient.post('/ai/chat-role/create', data);
} }
// 修改聊天角色 /** 修改聊天角色 */
export function updateChatRole(data: AiModelChatRoleApi.ChatRole) { export function updateChatRole(data: AiModelChatRoleApi.ChatRole) {
return requestClient.put('/ai/chat-role/update', data); return requestClient.put('/ai/chat-role/update', data);
} }
// 删除聊天角色 /** 删除聊天角色 */
export function deleteChatRole(id: number) { export function deleteChatRole(id: number) {
return requestClient.delete(`/ai/chat-role/delete?id=${id}`); return requestClient.delete(`/ai/chat-role/delete?id=${id}`);
} }
// ======= chat 聊天 /** 获取 my role */
// 获取 my role export function getMyPage(params: AiModelChatRoleApi.ChatRolePageReqVO) {
export function getMyPage(params: AiModelChatRoleApi.ChatRolePageReq) {
return requestClient.get('/ai/chat-role/my-page', { params }); return requestClient.get('/ai/chat-role/my-page', { params });
} }
// 获取角色分类 /** 获取角色分类 */
export function getCategoryList() { export function getCategoryList() {
return requestClient.get('/ai/chat-role/category-list'); return requestClient.get('/ai/chat-role/category-list');
} }
// 创建角色 /** 创建角色 */
export function createMy(data: AiModelChatRoleApi.ChatRole) { export function createMy(data: AiModelChatRoleApi.ChatRole) {
return requestClient.post('/ai/chat-role/create-my', data); return requestClient.post('/ai/chat-role/create-my', data);
} }
// 更新角色 /** 更新角色 */
export function updateMy(data: AiModelChatRoleApi.ChatRole) { export function updateMy(data: AiModelChatRoleApi.ChatRole) {
return requestClient.put('/ai/chat-role/update', data); return requestClient.put('/ai/chat-role/update', data);
} }
// 删除角色 my /** 删除角色 my */
export function deleteMy(id: number) { export function deleteMy(id: number) {
return requestClient.delete(`/ai/chat-role/delete-my?id=${id}`); return requestClient.delete(`/ai/chat-role/delete-my?id=${id}`);
} }

View File

@@ -18,7 +18,7 @@ export namespace AiModelModelApi {
} }
} }
// 查询模型分页 /** 查询模型分页 */
export function getModelPage(params: PageParam) { export function getModelPage(params: PageParam) {
return requestClient.get<PageResult<AiModelModelApi.Model>>( return requestClient.get<PageResult<AiModelModelApi.Model>>(
'/ai/model/page', '/ai/model/page',
@@ -26,7 +26,7 @@ export function getModelPage(params: PageParam) {
); );
} }
// 获得模型列表 /** 获得模型列表 */
export function getModelSimpleList(type?: number) { export function getModelSimpleList(type?: number) {
return requestClient.get<AiModelModelApi.Model[]>('/ai/model/simple-list', { return requestClient.get<AiModelModelApi.Model[]>('/ai/model/simple-list', {
params: { params: {
@@ -35,21 +35,22 @@ export function getModelSimpleList(type?: number) {
}); });
} }
// 查询模型详情 /** 查询模型详情 */
export function getModel(id: number) { export function getModel(id: number) {
return requestClient.get<AiModelModelApi.Model>(`/ai/model/get?id=${id}`); return requestClient.get<AiModelModelApi.Model>(`/ai/model/get?id=${id}`);
} }
// 新增模型
/** 新增模型 */
export function createModel(data: AiModelModelApi.Model) { export function createModel(data: AiModelModelApi.Model) {
return requestClient.post('/ai/model/create', data); return requestClient.post('/ai/model/create', data);
} }
// 修改模型 /** 修改模型 */
export function updateModel(data: AiModelModelApi.Model) { export function updateModel(data: AiModelModelApi.Model) {
return requestClient.put('/ai/model/update', data); return requestClient.put('/ai/model/update', data);
} }
// 删除模型 /** 删除模型 */
export function deleteModel(id: number) { export function deleteModel(id: number) {
return requestClient.delete(`/ai/model/delete?id=${id}`); return requestClient.delete(`/ai/model/delete?id=${id}`);
} }

View File

@@ -11,33 +11,34 @@ export namespace AiModelToolApi {
} }
} }
// 查询工具分页 /** 查询工具分页 */
export function getToolPage(params: PageParam) { export function getToolPage(params: PageParam) {
return requestClient.get<PageResult<AiModelToolApi.Tool>>('/ai/tool/page', { return requestClient.get<PageResult<AiModelToolApi.Tool>>('/ai/tool/page', {
params, params,
}); });
} }
// 查询工具详情 /** 查询工具详情 */
export function getTool(id: number) { export function getTool(id: number) {
return requestClient.get<AiModelToolApi.Tool>(`/ai/tool/get?id=${id}`); return requestClient.get<AiModelToolApi.Tool>(`/ai/tool/get?id=${id}`);
} }
// 新增工具
/** 新增工具 */
export function createTool(data: AiModelToolApi.Tool) { export function createTool(data: AiModelToolApi.Tool) {
return requestClient.post('/ai/tool/create', data); return requestClient.post('/ai/tool/create', data);
} }
// 修改工具 /** 修改工具 */
export function updateTool(data: AiModelToolApi.Tool) { export function updateTool(data: AiModelToolApi.Tool) {
return requestClient.put('/ai/tool/update', data); return requestClient.put('/ai/tool/update', data);
} }
// 删除工具 /** 删除工具 */
export function deleteTool(id: number) { export function deleteTool(id: number) {
return requestClient.delete(`/ai/tool/delete?id=${id}`); return requestClient.delete(`/ai/tool/delete?id=${id}`);
} }
// 获取工具简单列表 /** 获取工具简单列表 */
export function getToolSimpleList() { export function getToolSimpleList() {
return requestClient.get<AiModelToolApi.Tool[]>('/ai/tool/simple-list'); return requestClient.get<AiModelToolApi.Tool[]>('/ai/tool/simple-list');
} }

View File

@@ -9,8 +9,10 @@ import { requestClient } from '#/api/request';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
const accessStore = useAccessStore(); const accessStore = useAccessStore();
export namespace AiWriteApi { export namespace AiWriteApi {
export interface Write { export interface Write {
id?: number;
type: AiWriteTypeEnum.REPLY | AiWriteTypeEnum.WRITING; // 1:撰写 2:回复 type: AiWriteTypeEnum.REPLY | AiWriteTypeEnum.WRITING; // 1:撰写 2:回复
prompt: string; // 写作内容提示 1。撰写 2回复 prompt: string; // 写作内容提示 1。撰写 2回复
originalContent: string; // 原文 originalContent: string; // 原文
@@ -26,29 +28,12 @@ export namespace AiWriteApi {
createTime?: Date; // 创建时间 createTime?: Date; // 创建时间
} }
export interface AiWritePageReq extends PageParam { export interface AiWritePageReqVO extends PageParam {
userId?: number; // 用户编号 userId?: number; // 用户编号
type?: AiWriteTypeEnum; // 写作类型 type?: AiWriteTypeEnum; // 写作类型
platform?: string; // 平台 platform?: string; // 平台
createTime?: [string, string]; // 创建时间 createTime?: [string, string]; // 创建时间
} }
export interface AiWriteResp {
id: number;
userId: number;
type: number;
platform: string;
model: string;
prompt: string;
generatedContent: string;
originalContent: string;
length: number;
format: number;
tone: number;
language: number;
errorMessage: string;
createTime: string;
}
} }
export function writeStream({ export function writeStream({
@@ -80,15 +65,14 @@ export function writeStream({
}); });
} }
// 获取写作列表 /** 获取写作列表 */
export function getWritePage(params: any) { export function getWritePage(params: AiWriteApi.AiWritePageReqVO) {
return requestClient.get<PageResult<AiWriteApi.AiWritePageReq>>( return requestClient.get<PageResult<AiWriteApi.Write>>(`/ai/write/page`, {
`/ai/write/page`, params,
{ params }, });
);
} }
// 删除音乐 /** 删除写作记录 */
export function deleteWrite(id: number) { export function deleteWrite(id: number) {
return requestClient.delete(`/ai/write/delete`, { params: { id } }); return requestClient.delete(`/ai/write/delete`, { params: { id } });
} }

View File

@@ -133,6 +133,7 @@ export default defineComponent({
> >
{() => { {() => {
if (item.slot) { if (item.slot) {
// TODO @xingyu这里要 inline 掉么?
const slotContent = getSlot(slots, item.slot, data); const slotContent = getSlot(slots, item.slot, data);
return slotContent; return slotContent;
} }

View File

@@ -55,7 +55,7 @@ async function handleTabsClick(tab: any) {
/** 获取 my role 我的角色 */ /** 获取 my role 我的角色 */
async function getMyRole(append?: boolean) { async function getMyRole(append?: boolean) {
const params: AiModelChatRoleApi.ChatRolePageReq = { const params: AiModelChatRoleApi.ChatRolePageReqVO = {
...myRoleParams, ...myRoleParams,
name: search.value, name: search.value,
publicStatus: false, publicStatus: false,
@@ -70,7 +70,7 @@ async function getMyRole(append?: boolean) {
/** 获取 public role 公共角色 */ /** 获取 public role 公共角色 */
async function getPublicRole(append?: boolean) { async function getPublicRole(append?: boolean) {
const params: AiModelChatRoleApi.ChatRolePageReq = { const params: AiModelChatRoleApi.ChatRolePageReqVO = {
...publicRoleParams, ...publicRoleParams,
category: activeCategory.value === '全部' ? '' : activeCategory.value, category: activeCategory.value === '全部' ? '' : activeCategory.value,
name: search.value, name: search.value,

View File

@@ -1,11 +1,16 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/api/system/user';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { getSimpleUserList } from '#/api/system/user'; import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils'; import { getRangePickerDefaultProps } from '#/utils';
/** 关联数据 */
let userList: SystemUserApi.User[] = [];
getSimpleUserList().then((data) => (userList = data));
/** 列表的搜索表单 */ /** 列表的搜索表单 */
export function useGridFormSchemaConversation(): VbenFormSchema[] { export function useGridFormSchemaConversation(): VbenFormSchema[] {
return [ return [
@@ -13,11 +18,19 @@ export function useGridFormSchemaConversation(): VbenFormSchema[] {
fieldName: 'userId', fieldName: 'userId',
label: '用户编号', label: '用户编号',
component: 'Input', component: 'Input',
componentProps: {
placeholder: '请输入用户编号',
allowClear: true,
},
}, },
{ {
fieldName: 'title', fieldName: 'title',
label: '聊天标题', label: '聊天标题',
component: 'Input', component: 'Input',
componentProps: {
placeholder: '请输入聊天标题',
allowClear: true,
},
}, },
{ {
fieldName: 'createTime', fieldName: 'createTime',
@@ -49,7 +62,13 @@ export function useGridColumnsConversation(): VxeTableGridOptions['columns'] {
{ {
title: '用户', title: '用户',
minWidth: 180, minWidth: 180,
slots: { default: 'userId' }, field: 'userId',
formatter: ({ cellValue }) => {
if (cellValue === 0) {
return '系统';
}
return userList.find((user) => user.id === cellValue)?.nickname || '-';
},
}, },
{ {
field: 'roleName', field: 'roleName',
@@ -103,6 +122,10 @@ export function useGridFormSchemaMessage(): VbenFormSchema[] {
fieldName: 'conversationId', fieldName: 'conversationId',
label: '对话编号', label: '对话编号',
component: 'Input', component: 'Input',
componentProps: {
placeholder: '请输入对话编号',
allowClear: true,
},
}, },
{ {
fieldName: 'userId', fieldName: 'userId',
@@ -112,6 +135,8 @@ export function useGridFormSchemaMessage(): VbenFormSchema[] {
api: getSimpleUserList, api: getSimpleUserList,
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
placeholder: '请选择用户编号',
allowClear: true,
}, },
}, },
{ {
@@ -144,7 +169,9 @@ export function useGridColumnsMessage(): VxeTableGridOptions['columns'] {
{ {
title: '用户', title: '用户',
minWidth: 180, minWidth: 180,
slots: { default: 'userId' }, field: 'userId',
formatter: ({ cellValue }) =>
userList.find((user) => user.id === cellValue)?.nickname || '-',
}, },
{ {
field: 'roleName', field: 'roleName',

View File

@@ -3,10 +3,10 @@ import { ref } from 'vue';
import { DocAlert, Page } from '@vben/common-ui'; import { DocAlert, Page } from '@vben/common-ui';
import { Card, Tabs } from 'ant-design-vue'; import { Tabs } from 'ant-design-vue';
import ChatConversationList from './modules/ChatConversationList.vue'; import ChatConversationList from './modules/conversation-list.vue';
import ChatMessageList from './modules/ChatMessageList.vue'; import ChatMessageList from './modules/message-list.vue';
const activeTabName = ref('conversation'); const activeTabName = ref('conversation');
</script> </script>
@@ -16,15 +16,14 @@ const activeTabName = ref('conversation');
<template #doc> <template #doc>
<DocAlert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" /> <DocAlert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
</template> </template>
<Card>
<Tabs v-model:active-key="activeTabName"> <Tabs v-model:active-key="activeTabName">
<Tabs.TabPane tab="对话列表" key="conversation"> <Tabs.TabPane tab="对话列表" key="conversation">
<ChatConversationList /> <ChatConversationList />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab="消息列表" key="message"> <Tabs.TabPane tab="消息列表" key="message">
<ChatMessageList /> <ChatMessageList />
</Tabs.TabPane> </Tabs.TabPane>
</Tabs> </Tabs>
</Card>
</Page> </Page>
</template> </template>

View File

@@ -1,9 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiChatConversationApi } from '#/api/ai/chat/conversation'; import type { AiChatConversationApi } from '#/api/ai/chat/conversation';
import type { SystemUserApi } from '#/api/system/user';
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
@@ -14,7 +11,6 @@ import {
deleteChatConversationByAdmin, deleteChatConversationByAdmin,
getChatConversationPage, getChatConversationPage,
} from '#/api/ai/chat/conversation'; } from '#/api/ai/chat/conversation';
import { getSimpleUserList } from '#/api/system/user';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { import {
@@ -22,23 +18,20 @@ import {
useGridFormSchemaConversation, useGridFormSchemaConversation,
} from '../data'; } from '../data';
const userList = ref<SystemUserApi.User[]>([]); //
/** 刷新表格 */ /** 刷新表格 */
function handleRefresh() { function handleRefresh() {
gridApi.query(); gridApi.query();
} }
/** 删除 */ /** 删除对话 */
async function handleDelete(row: AiChatConversationApi.ChatConversation) { async function handleDelete(row: AiChatConversationApi.ChatConversation) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]), content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0, duration: 0,
}); });
try { try {
await deleteChatConversationByAdmin(row.id as number); await deleteChatConversationByAdmin(row.id!);
message.success({ message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
});
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();
@@ -66,32 +59,19 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<AiChatConversationApi.ChatConversation>, } as VxeTableGridOptions<AiChatConversationApi.ChatConversation>,
separator: false,
});
onMounted(async () => {
//
userList.value = await getSimpleUserList();
}); });
</script> </script>
<template> <template>
<Page auto-content-height> <Page auto-content-height>
<Grid table-title="对话列表"> <Grid table-title="对话列表">
<template #toolbar-tools>
<TableAction :actions="[]" />
</template>
<template #userId="{ row }">
<span>
{{ userList.find((item) => item.id === row.userId)?.nickname }}
</span>
<span v-if="row.userId === 0">系统</span>
</template>
<template #actions="{ row }"> <template #actions="{ row }">
<TableAction <TableAction
:actions="[ :actions="[

View File

@@ -1,9 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiChatConversationApi } from '#/api/ai/chat/conversation'; import type { AiChatConversationApi } from '#/api/ai/chat/conversation';
import type { SystemUserApi } from '#/api/system/user';
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
@@ -14,28 +11,24 @@ import {
deleteChatMessageByAdmin, deleteChatMessageByAdmin,
getChatMessagePage, getChatMessagePage,
} from '#/api/ai/chat/message'; } from '#/api/ai/chat/message';
import { getSimpleUserList } from '#/api/system/user';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useGridColumnsMessage, useGridFormSchemaMessage } from '../data'; import { useGridColumnsMessage, useGridFormSchemaMessage } from '../data';
const userList = ref<SystemUserApi.User[]>([]); //
/** 刷新表格 */ /** 刷新表格 */
function handleRefresh() { function handleRefresh() {
gridApi.query(); gridApi.query();
} }
/** 删除 */ /** 删除消息 */
async function handleDelete(row: AiChatConversationApi.ChatConversation) { async function handleDelete(row: AiChatConversationApi.ChatConversation) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]), content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0, duration: 0,
}); });
try { try {
await deleteChatMessageByAdmin(row.id as number); await deleteChatMessageByAdmin(row.id!);
message.success({ message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
});
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();
@@ -63,31 +56,19 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<AiChatConversationApi.ChatConversation>, } as VxeTableGridOptions<AiChatConversationApi.ChatConversation>,
separator: false,
});
onMounted(async () => {
//
userList.value = await getSimpleUserList();
}); });
</script> </script>
<template> <template>
<Page auto-content-height> <Page auto-content-height>
<Grid table-title="消息列表"> <Grid table-title="消息列表">
<template #toolbar-tools>
<TableAction :actions="[]" />
</template>
<template #userId="{ row }">
<span>
{{ userList.find((item) => item.id === row.userId)?.nickname }}
</span>
</template>
<template #actions="{ row }"> <template #actions="{ row }">
<TableAction <TableAction
:actions="[ :actions="[

View File

@@ -26,6 +26,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
api: getSimpleUserList, api: getSimpleUserList,
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
placeholder: '请选择用户编号',
allowClear: true,
}, },
}, },
{ {
@@ -33,6 +35,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '平台', label: '平台',
component: 'Select', component: 'Select',
componentProps: { componentProps: {
placeholder: '请选择平台',
allowClear: true, allowClear: true,
options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'), options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
}, },
@@ -42,6 +45,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '绘画状态', label: '绘画状态',
component: 'Select', component: 'Select',
componentProps: { componentProps: {
placeholder: '请选择绘画状态',
allowClear: true, allowClear: true,
options: getDictOptions(DICT_TYPE.AI_IMAGE_STATUS, 'number'), options: getDictOptions(DICT_TYPE.AI_IMAGE_STATUS, 'number'),
}, },
@@ -51,8 +55,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '是否发布', label: '是否发布',
component: 'Select', component: 'Select',
componentProps: { componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'), placeholder: '请选择是否发布',
allowClear: true, allowClear: true,
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
}, },
}, },
{ {
@@ -68,7 +73,12 @@ export function useGridFormSchema(): VbenFormSchema[] {
} }
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] { export function useGridColumns(
onPublicStatusChange?: (
newStatus: boolean,
row: any,
) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions['columns'] {
return [ return [
{ {
field: 'id', field: 'id',
@@ -118,7 +128,16 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{ {
minWidth: 100, minWidth: 100,
title: '是否发布', title: '是否发布',
slots: { default: 'publicStatus' }, field: 'publicStatus',
align: 'center',
cellRender: {
attrs: { beforeChange: onPublicStatusChange },
name: 'CellSwitch',
props: {
checkedValue: true,
unCheckedValue: false,
},
},
}, },
{ {
field: 'prompt', field: 'prompt',

View File

@@ -3,9 +3,8 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiImageApi } from '#/api/ai/image'; import type { AiImageApi } from '#/api/ai/image';
import { confirm, DocAlert, Page } from '@vben/common-ui'; import { confirm, DocAlert, Page } from '@vben/common-ui';
import { AiImageStatusEnum } from '@vben/constants';
import { message, Switch } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteImage, getImagePage, updateImage } from '#/api/ai/image'; import { deleteImage, getImagePage, updateImage } from '#/api/ai/image';
@@ -18,7 +17,7 @@ function handleRefresh() {
gridApi.query(); gridApi.query();
} }
/** 删除 */ /** 删除图片 */
async function handleDelete(row: AiImageApi.Image) { async function handleDelete(row: AiImageApi.Image) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]), content: $t('ui.actionMessage.deleting', [row.id]),
@@ -26,36 +25,45 @@ async function handleDelete(row: AiImageApi.Image) {
}); });
try { try {
await deleteImage(row.id as number); await deleteImage(row.id as number);
message.success({ message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
});
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();
} }
} }
/** 修改是否发布 */ /** 修改是否发布 */
async function handleUpdatePublicStatusChange(row: AiImageApi.Image) { async function handleUpdatePublicStatusChange(
try { newStatus: boolean,
// 修改状态的二次确认 row: AiImageApi.Image,
const text = row.publicStatus ? '公开' : '私有'; ): Promise<boolean | undefined> {
await confirm(`确认要"${text}"该图片吗?`).then(async () => { const text = newStatus ? '公开' : '私有';
await updateImage({ return new Promise((resolve, reject) => {
id: row.id, confirm({
publicStatus: row.publicStatus, content: `确认要将该图片切换为【${text}】吗?`,
})
.then(async () => {
// 更新图片状态
await updateImage({
id: row.id,
publicStatus: newStatus,
});
// 提示并返回成功
message.success($t('ui.actionMessage.operationSuccess'));
resolve(true);
})
.catch(() => {
reject(new Error('取消操作'));
}); });
handleRefresh(); });
});
} catch {
row.publicStatus = !row.publicStatus;
}
} }
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
gridOptions: { gridOptions: {
columns: useGridColumns(), columns: useGridColumns(handleUpdatePublicStatusChange),
height: 'auto', height: 'auto',
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
@@ -71,6 +79,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,
@@ -86,13 +95,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
<DocAlert title="AI 绘图创作" url="https://doc.iocoder.cn/ai/image/" /> <DocAlert title="AI 绘图创作" url="https://doc.iocoder.cn/ai/image/" />
</template> </template>
<Grid table-title="绘画管理列表"> <Grid table-title="绘画管理列表">
<template #publicStatus="{ row }">
<Switch
v-model:checked="row.publicStatus"
@change="handleUpdatePublicStatusChange(row)"
:disabled="row.status !== AiImageStatusEnum.SUCCESS"
/>
</template>
<template #actions="{ row }"> <template #actions="{ row }">
<TableAction <TableAction
:actions="[ :actions="[

View File

@@ -3,13 +3,15 @@ import type { AiMindmapApi } from '#/api/ai/mindmap';
import { nextTick, onMounted, ref } from 'vue'; import { nextTick, onMounted, ref } from 'vue';
import { alert, Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { MindMapContentExample } from '@vben/constants'; import { MindMapContentExample } from '@vben/constants';
import { message } from 'ant-design-vue';
import { generateMindMap } from '#/api/ai/mindmap'; import { generateMindMap } from '#/api/ai/mindmap';
import Left from './modules/Left.vue'; import Left from './modules/left.vue';
import Right from './modules/Right.vue'; import Right from './modules/right.vue';
const ctrl = ref<AbortController>(); // 请求控制 const ctrl = ref<AbortController>(); // 请求控制
const isGenerating = ref(false); // 是否正在生成思维导图 const isGenerating = ref(false); // 是否正在生成思维导图
@@ -26,8 +28,9 @@ function directGenerate(existPrompt: string) {
generatedContent.value = existPrompt; generatedContent.value = existPrompt;
isEnd.value = true; isEnd.value = true;
} }
/** 提交生成 */ /** 提交生成 */
function submit(data: AiMindmapApi.AiMindMapGenerateReq) { function handleSubmit(data: AiMindmapApi.AiMindMapGenerateReqVO) {
isGenerating.value = true; isGenerating.value = true;
isStart.value = true; isStart.value = true;
isEnd.value = false; isEnd.value = false;
@@ -38,8 +41,8 @@ function submit(data: AiMindmapApi.AiMindMapGenerateReq) {
onMessage: async (res: any) => { onMessage: async (res: any) => {
const { code, data, msg } = JSON.parse(res.data); const { code, data, msg } = JSON.parse(res.data);
if (code !== 0) { if (code !== 0) {
alert(`生成思维导图异常! ${msg}`); message.error(`生成思维导图异常! ${msg}`);
stopStream(); handleStopStream();
return; return;
} }
generatedContent.value = generatedContent.value + data; generatedContent.value = generatedContent.value + data;
@@ -49,19 +52,20 @@ function submit(data: AiMindmapApi.AiMindMapGenerateReq) {
onClose() { onClose() {
isEnd.value = true; isEnd.value = true;
leftRef.value?.setGeneratedContent(generatedContent.value); leftRef.value?.setGeneratedContent(generatedContent.value);
stopStream(); handleStopStream();
}, },
onError(err) { onError(err) {
console.error('生成思维导图失败', err); console.error('生成思维导图失败', err);
stopStream(); handleStopStream();
// 需要抛出异常,禁止重试 // 需要抛出异常,禁止重试
throw err; throw err;
}, },
ctrl: ctrl.value, ctrl: ctrl.value,
}); });
} }
/** 停止 stream 生成 */ /** 停止 stream 生成 */
function stopStream() { function handleStopStream() {
isGenerating.value = false; isGenerating.value = false;
isStart.value = false; isStart.value = false;
ctrl.value?.abort(); ctrl.value?.abort();
@@ -80,7 +84,7 @@ onMounted(() => {
ref="leftRef" ref="leftRef"
class="mr-4" class="mr-4"
:is-generating="isGenerating" :is-generating="isGenerating"
@submit="submit" @submit="handleSubmit"
@direct-generate="directGenerate" @direct-generate="directGenerate"
/> />
<Right <Right

View File

@@ -8,7 +8,9 @@ import { Button, Textarea } from 'ant-design-vue';
defineProps<{ defineProps<{
isGenerating: boolean; isGenerating: boolean;
}>(); }>();
const emits = defineEmits(['submit', 'directGenerate']); const emits = defineEmits(['submit', 'directGenerate']);
const formData = reactive({ const formData = reactive({
prompt: '', prompt: '',
}); });

View File

@@ -18,6 +18,7 @@ const props = defineProps<{
isGenerating: boolean; // isGenerating: boolean; //
isStart: boolean; // html isStart: boolean; // html
}>(); }>();
const md = MarkdownIt(); const md = MarkdownIt();
const contentRef = ref<HTMLDivElement>(); // header const contentRef = ref<HTMLDivElement>(); // header
const mdContainerRef = ref<HTMLDivElement>(); // markdown const mdContainerRef = ref<HTMLDivElement>(); // markdown
@@ -30,12 +31,14 @@ let markMap: Markmap | null = null;
const transformer = new Transformer(); const transformer = new Transformer();
let resizeObserver: null | ResizeObserver = null; let resizeObserver: null | ResizeObserver = null;
const initialized = false; const initialized = false;
/** 初始化 */
onMounted(() => { onMounted(() => {
resizeObserver = new ResizeObserver(() => { resizeObserver = new ResizeObserver(() => {
contentAreaHeight.value = contentRef.value?.clientHeight || 0; contentAreaHeight.value = contentRef.value?.clientHeight || 0;
// //
if (contentAreaHeight.value && !initialized) { if (contentAreaHeight.value && !initialized) {
/** 初始化思维导图 */ //
try { try {
if (!markMap) { if (!markMap) {
markMap = Markmap.create(svgRef.value!); markMap = Markmap.create(svgRef.value!);
@@ -52,11 +55,15 @@ onMounted(() => {
resizeObserver.observe(contentRef.value); resizeObserver.observe(contentRef.value);
} }
}); });
/** 卸载 */
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (resizeObserver && contentRef.value) { if (resizeObserver && contentRef.value) {
resizeObserver.unobserve(contentRef.value); resizeObserver.unobserve(contentRef.value);
} }
}); });
/** 监听 props 变化 */
watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => { watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => {
// markdown // markdown
if (isStart) { if (isStart) {
@@ -84,6 +91,7 @@ function update() {
console.error(error); console.error(error);
} }
} }
/** 处理内容 */ /** 处理内容 */
function processContent(text: string) { function processContent(text: string) {
const arr: string[] = []; const arr: string[] = [];
@@ -98,6 +106,7 @@ function processContent(text: string) {
} }
return arr.join('\n'); return arr.join('\n');
} }
/** 下载图片download SVG to png file */ /** 下载图片download SVG to png file */
function downloadImage() { function downloadImage() {
const svgElement = mindMapRef.value; const svgElement = mindMapRef.value;
@@ -112,6 +121,7 @@ function downloadImage() {
drawWithImageSize: false, drawWithImageSize: false,
}); });
} }
defineExpose({ defineExpose({
scrollBottom() { scrollBottom() {
mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight); mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight);
@@ -135,7 +145,6 @@ defineExpose({
</div> </div>
</template> </template>
<div ref="contentRef" class="hide-scroll-bar box-border h-full"> <div ref="contentRef" class="hide-scroll-bar box-border h-full">
<!--展示 markdown 的容器最终生成的是 html 字符串直接用 v-html 嵌入-->
<div <div
v-if="isGenerating" v-if="isGenerating"
ref="mdContainerRef" ref="mdContainerRef"
@@ -146,7 +155,6 @@ defineExpose({
v-html="html" v-html="html"
></div> ></div>
</div> </div>
<div ref="mindMapRef" class="wh-full"> <div ref="mindMapRef" class="wh-full">
<svg <svg
ref="svgRef" ref="svgRef"

View File

@@ -5,12 +5,9 @@ import type { SystemUserApi } from '#/api/system/user';
import { getSimpleUserList } from '#/api/system/user'; import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils'; import { getRangePickerDefaultProps } from '#/utils';
/** 关联数据 */
let userList: SystemUserApi.User[] = []; let userList: SystemUserApi.User[] = [];
async function getUserData() { getSimpleUserList().then((data) => (userList = data));
userList = await getSimpleUserList();
}
getUserData();
/** 列表的搜索表单 */ /** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] { export function useGridFormSchema(): VbenFormSchema[] {
@@ -23,12 +20,18 @@ export function useGridFormSchema(): VbenFormSchema[] {
api: getSimpleUserList, api: getSimpleUserList,
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
placeholder: '请选择用户',
allowClear: true,
}, },
}, },
{ {
fieldName: 'prompt', fieldName: 'prompt',
label: '提示词', label: '提示词',
component: 'Input', component: 'Input',
componentProps: {
placeholder: '请输入提示词',
clearable: true,
},
}, },
{ {
fieldName: 'createTime', fieldName: 'createTime',

View File

@@ -2,8 +2,6 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiMindmapApi } from '#/api/ai/mindmap'; import type { AiMindmapApi } from '#/api/ai/mindmap';
import { nextTick, ref } from 'vue';
import { DocAlert, Page, useVbenDrawer } from '@vben/common-ui'; import { DocAlert, Page, useVbenDrawer } from '@vben/common-ui';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
@@ -12,21 +10,21 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteMindMap, getMindMapPage } from '#/api/ai/mindmap'; import { deleteMindMap, getMindMapPage } from '#/api/ai/mindmap';
import { $t } from '#/locales'; import { $t } from '#/locales';
import Right from '../index/modules/Right.vue'; import Right from '../index/modules/right.vue';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
const previewContent = ref('');
const [Drawer, drawerApi] = useVbenDrawer({ const [Drawer, drawerApi] = useVbenDrawer({
header: false, header: false,
footer: false, footer: false,
destroyOnClose: true, destroyOnClose: true,
}); });
/** 刷新表格 */ /** 刷新表格 */
function handleRefresh() { function handleRefresh() {
gridApi.query(); gridApi.query();
} }
/** 删除 */ /** 删除思维导图记录 */
async function handleDelete(row: AiMindmapApi.MindMap) { async function handleDelete(row: AiMindmapApi.MindMap) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]), content: $t('ui.actionMessage.deleting', [row.id]),
@@ -34,14 +32,18 @@ async function handleDelete(row: AiMindmapApi.MindMap) {
}); });
try { try {
await deleteMindMap(row.id as number); await deleteMindMap(row.id as number);
message.success({ message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
});
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();
} }
} }
/** 预览思维导图 */
async function openPreview(row: AiMindmapApi.MindMap) {
drawerApi.setData(row.generatedContent).open();
}
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
@@ -63,6 +65,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,
@@ -70,11 +73,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
} as VxeTableGridOptions<AiMindmapApi.MindMap>, } as VxeTableGridOptions<AiMindmapApi.MindMap>,
}); });
async function openPreview(row: AiMindmapApi.MindMap) {
previewContent.value = row.generatedContent;
drawerApi.open();
await nextTick();
}
</script> </script>
<template> <template>
@@ -82,9 +80,10 @@ async function openPreview(row: AiMindmapApi.MindMap) {
<template #doc> <template #doc>
<DocAlert title="AI 思维导图" url="https://doc.iocoder.cn/ai/mindmap/" /> <DocAlert title="AI 思维导图" url="https://doc.iocoder.cn/ai/mindmap/" />
</template> </template>
<Drawer class="w-3/5"> <Drawer class="w-3/5">
<Right <Right
:generated-content="previewContent" :generated-content="drawerApi.getData() as any"
:is-end="true" :is-end="true"
:is-generating="false" :is-generating="false"
:is-start="false" :is-start="false"
@@ -99,6 +98,7 @@ async function openPreview(row: AiMindmapApi.MindMap) {
type: 'link', type: 'link',
icon: ACTION_ICON.EDIT, icon: ACTION_ICON.EDIT,
auth: ['ai:api-key:update'], auth: ['ai:api-key:update'],
disabled: !row.generatedContent,
onClick: openPreview.bind(null, row), onClick: openPreview.bind(null, row),
}, },
{ {

View File

@@ -5,6 +5,7 @@ import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
/** 新增/修改的表单 */ /** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(): VbenFormSchema[] {
return [ return [
@@ -25,24 +26,33 @@ export function useFormSchema(): VbenFormSchema[] {
options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'), options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
allowClear: true, allowClear: true,
}, },
rules: z.string().min(1, { message: '请输入平台' }), rules: 'required',
}, },
{ {
component: 'Input', component: 'Input',
fieldName: 'name', fieldName: 'name',
label: '名称', label: '名称',
rules: 'required', rules: 'required',
componentProps: {
placeholder: '请输入名称',
},
}, },
{ {
component: 'Input', component: 'Input',
fieldName: 'apiKey', fieldName: 'apiKey',
label: '密钥', label: '密钥',
rules: 'required', rules: 'required',
componentProps: {
placeholder: '请输入密钥',
},
}, },
{ {
component: 'Input', component: 'Input',
fieldName: 'url', fieldName: 'url',
label: '自定义 API URL', label: '自定义 API URL',
componentProps: {
placeholder: '请输入自定义 API URL',
},
}, },
{ {
fieldName: 'status', fieldName: 'status',
@@ -65,6 +75,10 @@ export function useGridFormSchema(): VbenFormSchema[] {
fieldName: 'name', fieldName: 'name',
label: '名称', label: '名称',
component: 'Input', component: 'Input',
componentProps: {
placeholder: '请输入名称',
allowClear: true,
},
}, },
{ {
fieldName: 'platform', fieldName: 'platform',
@@ -72,6 +86,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'Select', component: 'Select',
componentProps: { componentProps: {
allowClear: true, allowClear: true,
placeholder: '请选择平台',
options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'), options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
}, },
}, },
@@ -81,6 +96,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'Select', component: 'Select',
componentProps: { componentProps: {
allowClear: true, allowClear: true,
placeholder: '请选择状态',
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
}, },
}, },
@@ -97,18 +113,22 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
name: 'CellDict', name: 'CellDict',
props: { type: DICT_TYPE.AI_PLATFORM }, props: { type: DICT_TYPE.AI_PLATFORM },
}, },
minWidth: 100,
}, },
{ {
field: 'name', field: 'name',
title: '名称', title: '名称',
minWidth: 120,
}, },
{ {
field: 'apiKey', field: 'apiKey',
title: '密钥', title: '密钥',
minWidth: 140,
}, },
{ {
field: 'url', field: 'url',
title: '自定义 API URL', title: '自定义 API URL',
minWidth: 180,
}, },
{ {
field: 'status', field: 'status',
@@ -117,6 +137,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
name: 'CellDict', name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS }, props: { type: DICT_TYPE.COMMON_STATUS },
}, },
minWidth: 80,
}, },
{ {
title: '操作', title: '操作',

View File

@@ -23,27 +23,25 @@ function handleRefresh() {
gridApi.query(); gridApi.query();
} }
/** 创建 */ /** 创建 API 密钥 */
function handleCreate() { function handleCreate() {
formModalApi.setData(null).open(); formModalApi.setData(null).open();
} }
/** 编辑 */ /** 编辑 API 密钥 */
function handleEdit(row: AiModelApiKeyApi.ApiKey) { function handleEdit(row: AiModelApiKeyApi.ApiKey) {
formModalApi.setData(row).open(); formModalApi.setData(row).open();
} }
/** 删除 */ /** 删除 API 密钥 */
async function handleDelete(row: AiModelApiKeyApi.ApiKey) { async function handleDelete(row: AiModelApiKeyApi.ApiKey) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0, duration: 0,
}); });
try { try {
await deleteApiKey(row.id as number); await deleteApiKey(row.id!);
message.success({ message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
});
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();
@@ -71,6 +69,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,

View File

@@ -27,7 +27,7 @@ const [Form, formApi] = useVbenForm({
class: 'w-full', class: 'w-full',
}, },
formItemClass: 'col-span-2', formItemClass: 'col-span-2',
labelWidth: 100, labelWidth: 120,
}, },
layout: 'horizontal', layout: 'horizontal',
schema: useFormSchema(), schema: useFormSchema(),
@@ -76,7 +76,7 @@ const [Modal, modalApi] = useVbenModal({
</script> </script>
<template> <template>
<Modal class="w-2/5" :title="getTitle"> <Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" /> <Form class="mx-4" />
</Modal> </Modal>
</template> </template>

View File

@@ -8,6 +8,7 @@ import { z } from '#/adapter/form';
import { getSimpleKnowledgeList } from '#/api/ai/knowledge/knowledge'; import { getSimpleKnowledgeList } from '#/api/ai/knowledge/knowledge';
import { getModelSimpleList } from '#/api/ai/model/model'; import { getModelSimpleList } from '#/api/ai/model/model';
import { getToolSimpleList } from '#/api/ai/model/tool'; import { getToolSimpleList } from '#/api/ai/model/tool';
/** 新增/修改的表单 */ /** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(): VbenFormSchema[] {
return [ return [
@@ -32,6 +33,9 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'name', fieldName: 'name',
label: '角色名称', label: '角色名称',
rules: 'required', rules: 'required',
componentProps: {
placeholder: '请输入角色名称',
},
}, },
{ {
component: 'ImageUpload', component: 'ImageUpload',
@@ -62,6 +66,9 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'category', fieldName: 'category',
label: '角色类别', label: '角色类别',
rules: 'required', rules: 'required',
componentProps: {
placeholder: '请输入角色类别',
},
dependencies: { dependencies: {
triggerFields: ['formType'], triggerFields: ['formType'],
show: (values) => { show: (values) => {
@@ -113,6 +120,17 @@ export function useFormSchema(): VbenFormSchema[] {
allowClear: true, allowClear: true,
}, },
}, },
{
fieldName: 'mcpClientNames',
label: '引用 MCP',
component: 'Select',
componentProps: {
placeholder: '请选择 MCP',
options: getDictOptions(DICT_TYPE.AI_MCP_CLIENT_NAME, 'string'),
mode: 'multiple',
allowClear: true,
},
},
{ {
fieldName: 'publicStatus', fieldName: 'publicStatus',
label: '是否公开', label: '是否公开',
@@ -137,7 +155,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber', component: 'InputNumber',
componentProps: { componentProps: {
placeholder: '请输入角色排序', placeholder: '请输入角色排序',
class: 'w-full',
}, },
dependencies: { dependencies: {
triggerFields: ['formType'], triggerFields: ['formType'],
@@ -254,6 +271,16 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
: `引用${cellValue.length}`; : `引用${cellValue.length}`;
}, },
}, },
{
title: 'MCP',
field: 'mcpClientNames',
minWidth: 100,
formatter: ({ cellValue }) => {
return !cellValue || cellValue.length === 0
? '-'
: `引用${cellValue.length}`;
},
},
{ {
field: 'publicStatus', field: 'publicStatus',
title: '是否公开', title: '是否公开',

View File

@@ -23,27 +23,25 @@ function handleRefresh() {
gridApi.query(); gridApi.query();
} }
/** 创建 */ /** 创建聊天角色 */
function handleCreate() { function handleCreate() {
formModalApi.setData({ formType: 'create' }).open(); formModalApi.setData({ formType: 'create' }).open();
} }
/** 编辑 */ /** 编辑聊天角色 */
function handleEdit(row: AiModelChatRoleApi.ChatRole) { function handleEdit(row: AiModelChatRoleApi.ChatRole) {
formModalApi.setData({ formType: 'update', ...row }).open(); formModalApi.setData({ formType: 'update', ...row }).open();
} }
/** 删除 */ /** 删除聊天角色 */
async function handleDelete(row: AiModelChatRoleApi.ChatRole) { async function handleDelete(row: AiModelChatRoleApi.ChatRole) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0, duration: 0,
}); });
try { try {
await deleteChatRole(row.id as number); await deleteChatRole(row.id!);
message.success({ message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
});
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();
@@ -71,6 +69,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,

View File

@@ -82,7 +82,7 @@ const [Modal, modalApi] = useVbenModal({
</script> </script>
<template> <template>
<Modal class="w-2/5" :title="getTitle"> <Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" /> <Form class="mx-4" />
</Modal> </Modal>
</template> </template>

View File

@@ -8,12 +8,9 @@ import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { getApiKeySimpleList } from '#/api/ai/model/apiKey'; import { getApiKeySimpleList } from '#/api/ai/model/apiKey';
/** 关联数据 */
let apiKeyList: AiModelApiKeyApi.ApiKey[] = []; let apiKeyList: AiModelApiKeyApi.ApiKey[] = [];
async function getApiKeyList() { getApiKeySimpleList().then((data) => (apiKeyList = data));
apiKeyList = await getApiKeySimpleList();
}
getApiKeyList();
/** 新增/修改的表单 */ /** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(): VbenFormSchema[] {
@@ -35,7 +32,7 @@ export function useFormSchema(): VbenFormSchema[] {
options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'), options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
allowClear: true, allowClear: true,
}, },
rules: z.string().min(1, { message: '请输入平台' }), rules: 'required',
}, },
{ {
fieldName: 'type', fieldName: 'type',
@@ -56,7 +53,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: 'API 秘钥', label: 'API 秘钥',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
placeholder: '请选择API 秘钥', placeholder: '请选择 API 秘钥',
api: getApiKeySimpleList, api: getApiKeySimpleList,
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
@@ -69,12 +66,18 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'name', fieldName: 'name',
label: '模型名字', label: '模型名字',
rules: 'required', rules: 'required',
componentProps: {
placeholder: '请输入模型名字',
},
}, },
{ {
component: 'Input', component: 'Input',
fieldName: 'model', fieldName: 'model',
label: '模型标识', label: '模型标识',
rules: 'required', rules: 'required',
componentProps: {
placeholder: '请输入模型标识',
},
}, },
{ {
fieldName: 'sort', fieldName: 'sort',
@@ -82,7 +85,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber', component: 'InputNumber',
componentProps: { componentProps: {
placeholder: '请输入模型排序', placeholder: '请输入模型排序',
class: 'w-full',
}, },
rules: 'required', rules: 'required',
}, },
@@ -103,7 +105,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber', component: 'InputNumber',
componentProps: { componentProps: {
placeholder: '请输入温度参数', placeholder: '请输入温度参数',
class: 'w-full',
min: 0, min: 0,
max: 2, max: 2,
}, },
@@ -123,7 +124,6 @@ export function useFormSchema(): VbenFormSchema[] {
min: 0, min: 0,
max: 8192, max: 8192,
placeholder: '请输入回复数 Token 数', placeholder: '请输入回复数 Token 数',
class: 'w-full',
}, },
dependencies: { dependencies: {
triggerFields: ['type'], triggerFields: ['type'],
@@ -141,7 +141,6 @@ export function useFormSchema(): VbenFormSchema[] {
min: 0, min: 0,
max: 20, max: 20,
placeholder: '请输入上下文数量', placeholder: '请输入上下文数量',
class: 'w-full',
}, },
dependencies: { dependencies: {
triggerFields: ['type'], triggerFields: ['type'],
@@ -161,16 +160,28 @@ export function useGridFormSchema(): VbenFormSchema[] {
fieldName: 'name', fieldName: 'name',
label: '模型名字', label: '模型名字',
component: 'Input', component: 'Input',
componentProps: {
placeholder: '请输入模型名字',
allowClear: true,
},
}, },
{ {
fieldName: 'model', fieldName: 'model',
label: '模型标识', label: '模型标识',
component: 'Input', component: 'Input',
componentProps: {
placeholder: '请输入模型标识',
allowClear: true,
},
}, },
{ {
fieldName: 'platform', fieldName: 'platform',
label: '模型平台', label: '模型平台',
component: 'Input', component: 'Input',
componentProps: {
placeholder: '请输入模型平台',
allowClear: true,
},
}, },
]; ];
} }
@@ -233,7 +244,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{ {
field: 'temperature', field: 'temperature',
title: '温度参数', title: '温度参数',
minWidth: 80, minWidth: 100,
}, },
{ {
title: '回复数 Token 数', title: '回复数 Token 数',
@@ -243,7 +254,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{ {
title: '上下文数量', title: '上下文数量',
field: 'maxContexts', field: 'maxContexts',
minWidth: 100, minWidth: 120,
}, },
{ {
title: '操作', title: '操作',

View File

@@ -23,27 +23,25 @@ function handleRefresh() {
gridApi.query(); gridApi.query();
} }
/** 创建 */ /** 创建模型配置 */
function handleCreate() { function handleCreate() {
formModalApi.setData(null).open(); formModalApi.setData(null).open();
} }
/** 编辑 */ /** 编辑模型配置 */
function handleEdit(row: AiModelModelApi.Model) { function handleEdit(row: AiModelModelApi.Model) {
formModalApi.setData(row).open(); formModalApi.setData(row).open();
} }
/** 删除 */ /** 删除模型配置 */
async function handleDelete(row: AiModelModelApi.Model) { async function handleDelete(row: AiModelModelApi.Model) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0, duration: 0,
}); });
try { try {
await deleteModel(row.id as number); await deleteModel(row.id!);
message.success({ message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
});
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();
@@ -71,6 +69,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,

View File

@@ -77,7 +77,7 @@ const [Modal, modalApi] = useVbenModal({
</script> </script>
<template> <template>
<Modal class="w-2/5" :title="getTitle"> <Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" /> <Form class="mx-4" />
</Modal> </Modal>
</template> </template>

View File

@@ -22,6 +22,9 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'name', fieldName: 'name',
label: '工具名称', label: '工具名称',
rules: 'required', rules: 'required',
componentProps: {
placeholder: '请输入工具名称',
},
}, },
{ {
component: 'Textarea', component: 'Textarea',
@@ -52,6 +55,10 @@ export function useGridFormSchema(): VbenFormSchema[] {
fieldName: 'name', fieldName: 'name',
label: '工具名称', label: '工具名称',
component: 'Input', component: 'Input',
componentProps: {
placeholder: '请输入工具名称',
allowClear: true,
},
}, },
{ {
fieldName: 'status', fieldName: 'status',
@@ -60,6 +67,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
allowClear: true, allowClear: true,
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
placeholder: '请选择状态',
}, },
}, },
{ {
@@ -80,14 +88,17 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{ {
field: 'id', field: 'id',
title: '工具编号', title: '工具编号',
minWidth: 100,
}, },
{ {
field: 'name', field: 'name',
title: '工具名称', title: '工具名称',
minWidth: 120,
}, },
{ {
field: 'description', field: 'description',
title: '工具描述', title: '工具描述',
minWidth: 140,
}, },
{ {
field: 'status', field: 'status',
@@ -96,6 +107,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
name: 'CellDict', name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS }, props: { type: DICT_TYPE.COMMON_STATUS },
}, },
minWidth: 80,
}, },
{ {
field: 'createTime', field: 'createTime',

View File

@@ -23,27 +23,25 @@ function handleRefresh() {
gridApi.query(); gridApi.query();
} }
/** 创建 */ /** 创建工具 */
function handleCreate() { function handleCreate() {
formModalApi.setData(null).open(); formModalApi.setData(null).open();
} }
/** 编辑 */ /** 编辑工具 */
function handleEdit(row: AiModelToolApi.Tool) { function handleEdit(row: AiModelToolApi.Tool) {
formModalApi.setData(row).open(); formModalApi.setData(row).open();
} }
/** 删除 */ /** 删除工具 */
async function handleDelete(row: AiModelToolApi.Tool) { async function handleDelete(row: AiModelToolApi.Tool) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0, duration: 0,
}); });
try { try {
await deleteTool(row.id as number); await deleteTool(row.id!);
message.success({ message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
});
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();
@@ -71,6 +69,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,

View File

@@ -76,7 +76,7 @@ const [Modal, modalApi] = useVbenModal({
</script> </script>
<template> <template>
<Modal class="w-2/5" :title="getTitle"> <Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" /> <Form class="mx-4" />
</Modal> </Modal>
</template> </template>

View File

@@ -8,12 +8,9 @@ import { getDictOptions } from '@vben/hooks';
import { getSimpleUserList } from '#/api/system/user'; import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils'; import { getRangePickerDefaultProps } from '#/utils';
/** 关联数据 */
let userList: SystemUserApi.User[] = []; let userList: SystemUserApi.User[] = [];
async function getUserData() { getSimpleUserList().then((data) => (userList = data));
userList = await getSimpleUserList();
}
getUserData();
/** 列表的搜索表单 */ /** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] { export function useGridFormSchema(): VbenFormSchema[] {
@@ -26,18 +23,25 @@ export function useGridFormSchema(): VbenFormSchema[] {
api: getSimpleUserList, api: getSimpleUserList,
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
placeholder: '请选择用户编号',
allowClear: true,
}, },
}, },
{ {
fieldName: 'title', fieldName: 'title',
label: '音乐名称', label: '音乐名称',
component: 'Input', component: 'Input',
componentProps: {
placeholder: '请输入音乐名称',
allowClear: true,
},
}, },
{ {
fieldName: 'status', fieldName: 'status',
label: '绘画状态', label: '绘画状态',
component: 'Select', component: 'Select',
componentProps: { componentProps: {
placeholder: '请选择绘画状态',
allowClear: true, allowClear: true,
options: getDictOptions(DICT_TYPE.AI_MUSIC_STATUS, 'number'), options: getDictOptions(DICT_TYPE.AI_MUSIC_STATUS, 'number'),
}, },
@@ -47,6 +51,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '生成模式', label: '生成模式',
component: 'Select', component: 'Select',
componentProps: { componentProps: {
placeholder: '请选择生成模式',
allowClear: true, allowClear: true,
options: getDictOptions(DICT_TYPE.AI_GENERATE_MODE, 'number'), options: getDictOptions(DICT_TYPE.AI_GENERATE_MODE, 'number'),
}, },
@@ -65,15 +70,21 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '是否发布', label: '是否发布',
component: 'Select', component: 'Select',
componentProps: { componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'), placeholder: '请选择是否发布',
allowClear: true, allowClear: true,
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
}, },
}, },
]; ];
} }
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] { export function useGridColumns(
onPublicStatusChange?: (
newStatus: boolean,
row: any,
) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions['columns'] {
return [ return [
{ {
field: 'id', field: 'id',
@@ -154,7 +165,16 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{ {
minWidth: 100, minWidth: 100,
title: '是否发布', title: '是否发布',
slots: { default: 'publicStatus' }, field: 'publicStatus',
align: 'center',
cellRender: {
attrs: { beforeChange: onPublicStatusChange },
name: 'CellSwitch',
props: {
checkedValue: true,
unCheckedValue: false,
},
},
}, },
{ {
field: 'taskId', field: 'taskId',

View File

@@ -3,9 +3,8 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiMusicApi } from '#/api/ai/music'; import type { AiMusicApi } from '#/api/ai/music';
import { confirm, DocAlert, Page } from '@vben/common-ui'; import { confirm, DocAlert, Page } from '@vben/common-ui';
import { AiMusicStatusEnum } from '@vben/constants';
import { Button, message, Switch } from 'ant-design-vue'; import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteMusic, getMusicPage, updateMusic } from '#/api/ai/music'; import { deleteMusic, getMusicPage, updateMusic } from '#/api/ai/music';
@@ -18,7 +17,7 @@ function handleRefresh() {
gridApi.query(); gridApi.query();
} }
/** 删除 */ /** 删除音乐记录 */
async function handleDelete(row: AiMusicApi.Music) { async function handleDelete(row: AiMusicApi.Music) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]), content: $t('ui.actionMessage.deleting', [row.id]),
@@ -26,36 +25,45 @@ async function handleDelete(row: AiMusicApi.Music) {
}); });
try { try {
await deleteMusic(row.id as number); await deleteMusic(row.id as number);
message.success({ message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
});
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();
} }
} }
/** 修改是否发布 */ /** 修改是否发布 */
const handleUpdatePublicStatusChange = async (row: AiMusicApi.Music) => { async function handleUpdatePublicStatusChange(
try { newStatus: boolean,
// 修改状态的二次确认 row: AiMusicApi.Music,
const text = row.publicStatus ? '公开' : '私有'; ): Promise<boolean | undefined> {
await confirm(`确认要"${text}"该图片吗?`).then(async () => { const text = newStatus ? '公开' : '私有';
await updateMusic({ return new Promise((resolve, reject) => {
id: row.id, confirm({
publicStatus: row.publicStatus, content: `确认要将该音乐切换为【${text}】吗?`,
})
.then(async () => {
// 更新音乐状态
await updateMusic({
id: row.id,
publicStatus: newStatus,
});
// 提示并返回成功
message.success($t('ui.actionMessage.operationSuccess'));
resolve(true);
})
.catch(() => {
reject(new Error('取消操作'));
}); });
handleRefresh(); });
}); }
} catch {
row.publicStatus = !row.publicStatus;
}
};
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
gridOptions: { gridOptions: {
columns: useGridColumns(), columns: useGridColumns(handleUpdatePublicStatusChange),
height: 'auto', height: 'auto',
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
@@ -71,6 +79,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,
@@ -119,13 +128,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
封面 封面
</Button> </Button>
</template> </template>
<template #publicStatus="{ row }">
<Switch
v-model:checked="row.publicStatus"
@change="handleUpdatePublicStatusChange(row)"
:disabled="row.status !== AiMusicStatusEnum.SUCCESS"
/>
</template>
<template #actions="{ row }"> <template #actions="{ row }">
<TableAction <TableAction
:actions="[ :actions="[

View File

@@ -3,28 +3,24 @@ import type { AiWriteApi } from '#/api/ai/write';
import { nextTick, ref } from 'vue'; import { nextTick, ref } from 'vue';
import { alert, Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { WriteExample } from '@vben/constants'; import { WriteExample } from '@vben/constants';
import { message } from 'ant-design-vue';
import { writeStream } from '#/api/ai/write'; import { writeStream } from '#/api/ai/write';
import Left from './components/Left.vue'; import Left from './modules/left.vue';
import Right from './components/Right.vue'; import Right from './modules/right.vue';
const writeResult = ref(''); // 写作结果 const writeResult = ref(''); // 写作结果
const isWriting = ref(false); // 是否正在写作中 const isWriting = ref(false); // 是否正在写作中
const abortController = ref<AbortController>(); // // 写作进行中 abort 控制器(控制 stream 写作) const abortController = ref<AbortController>(); // // 写作进行中 abort 控制器(控制 stream 写作)
/** 停止 stream 生成 */ const rightRef = ref<InstanceType<typeof Right>>(); // 写作面板
function stopStream() {
abortController.value?.abort();
isWriting.value = false;
}
/** 执行写作 */ /** 提交写作 */
const rightRef = ref<InstanceType<typeof Right>>(); function handleSubmit(data: Partial<AiWriteApi.Write>) {
function submit(data: Partial<AiWriteApi.Write>) {
abortController.value = new AbortController(); abortController.value = new AbortController();
writeResult.value = ''; writeResult.value = '';
isWriting.value = true; isWriting.value = true;
@@ -33,8 +29,8 @@ function submit(data: Partial<AiWriteApi.Write>) {
onMessage: async (res: any) => { onMessage: async (res: any) => {
const { code, data, msg } = JSON.parse(res.data); const { code, data, msg } = JSON.parse(res.data);
if (code !== 0) { if (code !== 0) {
alert(`写作异常! ${msg}`); message.error(`写作异常! ${msg}`);
stopStream(); handleStopStream();
return; return;
} }
writeResult.value = writeResult.value + data; writeResult.value = writeResult.value + data;
@@ -43,23 +39,29 @@ function submit(data: Partial<AiWriteApi.Write>) {
rightRef.value?.scrollToBottom(); rightRef.value?.scrollToBottom();
}, },
ctrl: abortController.value, ctrl: abortController.value,
onClose: stopStream, onClose: handleStopStream,
onError: (error: any) => { onError: (error: any) => {
console.error('写作异常', error); console.error('写作异常', error);
stopStream(); handleStopStream();
// 需要抛出异常,禁止重试 // 需要抛出异常,禁止重试
throw error; throw error;
}, },
}); });
} }
/** 停止 stream 生成 */
function handleStopStream() {
abortController.value?.abort();
isWriting.value = false;
}
/** 点击示例触发 */ /** 点击示例触发 */
function handleExampleClick(type: keyof typeof WriteExample) { function handleExampleClick(type: keyof typeof WriteExample) {
writeResult.value = WriteExample[type].data; writeResult.value = WriteExample[type].data;
} }
/** 点击重置的时候清空写作的结果*/ /** 点击重置的时候清空写作的结果*/
function reset() { function handleReset() {
writeResult.value = ''; writeResult.value = '';
} }
</script> </script>
@@ -70,13 +72,13 @@ function reset() {
<Left <Left
:is-writing="isWriting" :is-writing="isWriting"
class="mr-4 h-full rounded-lg" class="mr-4 h-full rounded-lg"
@submit="submit" @submit="handleSubmit"
@reset="reset" @reset="handleReset"
@example="handleExampleClick" @example="handleExampleClick"
/> />
<Right <Right
:is-writing="isWriting" :is-writing="isWriting"
@stop-stream="stopStream" @stop-stream="handleStopStream"
ref="rightRef" ref="rightRef"
class="flex-grow" class="flex-grow"
v-model:content="writeResult" v-model:content="writeResult"

View File

@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
// TODO @gjd modules
import type { AiWriteApi } from '#/api/ai/write'; import type { AiWriteApi } from '#/api/ai/write';
import { ref } from 'vue'; import { ref } from 'vue';
@@ -11,7 +10,7 @@ import { IconifyIcon } from '@vben/icons';
import { createReusableTemplate } from '@vueuse/core'; import { createReusableTemplate } from '@vueuse/core';
import { Button, message, Textarea } from 'ant-design-vue'; import { Button, message, Textarea } from 'ant-design-vue';
import Tag from './Tag.vue'; import Tag from './tag.vue';
type TabType = AiWriteApi.Write['type']; type TabType = AiWriteApi.Write['type'];
@@ -34,6 +33,7 @@ function omit(obj: Record<string, any>, keysToOmit: string[]) {
} }
return result; return result;
} }
/** 点击示例的时候,将定义好的文章作为示例展示出来 */ /** 点击示例的时候,将定义好的文章作为示例展示出来 */
function example(type: 'reply' | 'write') { function example(type: 'reply' | 'write') {
formData.value = { formData.value = {
@@ -79,13 +79,11 @@ const initData: AiWriteApi.Write = {
length: 1, length: 1,
format: 1, format: 1,
}; };
const formData = ref<AiWriteApi.Write>({ ...initData }); const formData = ref<AiWriteApi.Write>({ ...initData });
const recordFormData = {} as Record<AiWriteTypeEnum, AiWriteApi.Write>; //
/** 用来记录切换之前所填写的数据,切换的时候给赋值回来 */ /** 切换 tab */
const recordFormData = {} as Record<AiWriteTypeEnum, AiWriteApi.Write>; function handleSwitchTab(value: TabType) {
/** 切换tab */
function switchTab(value: TabType) {
if (value !== selectedTab.value) { if (value !== selectedTab.value) {
// //
recordFormData[selectedTab.value] = formData.value; recordFormData[selectedTab.value] = formData.value;
@@ -96,8 +94,11 @@ function switchTab(value: TabType) {
} }
/** 提交写作 */ /** 提交写作 */
function submit() { function handleSubmit() {
if (selectedTab.value === 2 && !formData.value.originalContent) { if (
selectedTab.value === AiWriteTypeEnum.REPLY &&
!formData.value.originalContent
) {
message.warning('请输入原文'); message.warning('请输入原文');
return; return;
} }
@@ -105,12 +106,13 @@ function submit() {
message.warning(`请输入${selectedTab.value === 1 ? '写作' : '回复'}内容`); message.warning(`请输入${selectedTab.value === 1 ? '写作' : '回复'}内容`);
return; return;
} }
emit('submit', { emit('submit', {
/** 撰写的时候没有 originalContent 字段*/ // originalContent
...(selectedTab.value === 1 ...(selectedTab.value === 1
? omit(formData.value, ['originalContent']) ? omit(formData.value, ['originalContent'])
: formData.value), : formData.value),
/** 使用选中 tab 值覆盖当前的 type 类型 */ // 使 tab type
type: selectedTab.value, type: selectedTab.value,
}); });
} }
@@ -156,7 +158,7 @@ function submit() {
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.value" :key="tab.value"
:active="tab.value === selectedTab" :active="tab.value === selectedTab"
:item-click="() => switchTab(tab.value)" :item-click="() => handleSwitchTab(tab.value)"
:text="tab.text" :text="tab.text"
class="relative z-20" class="relative z-20"
/> />
@@ -167,7 +169,7 @@ function submit() {
class="bg-card box-border h-full w-96 flex-grow overflow-y-auto px-7 pb-2 lg:block" class="bg-card box-border h-full w-96 flex-grow overflow-y-auto px-7 pb-2 lg:block"
> >
<div> <div>
<template v-if="selectedTab === 1"> <template v-if="selectedTab === AiWriteTypeEnum.WRITING">
<ReuseLabel <ReuseLabel
:hint-click="() => example('write')" :hint-click="() => example('write')"
hint="示例" hint="示例"
@@ -181,7 +183,6 @@ function submit() {
show-count show-count
/> />
</template> </template>
<template v-else> <template v-else>
<ReuseLabel <ReuseLabel
:hint-click="() => example('reply')" :hint-click="() => example('reply')"
@@ -195,7 +196,6 @@ function submit() {
placeholder="请输入原文" placeholder="请输入原文"
show-count show-count
/> />
<ReuseLabel label="回复内容" /> <ReuseLabel label="回复内容" />
<Textarea <Textarea
v-model:value="formData.prompt" v-model:value="formData.prompt"
@@ -231,7 +231,7 @@ function submit() {
<Button :disabled="isWriting" class="mr-2" @click="reset"> <Button :disabled="isWriting" class="mr-2" @click="reset">
重置 重置
</Button> </Button>
<Button type="primary" :loading="isWriting" @click="submit"> <Button type="primary" :loading="isWriting" @click="handleSubmit">
生成 生成
</Button> </Button>
</div> </div>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
// TODO @gjd modules
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
@@ -9,16 +8,15 @@ import { Button, Card, message, Textarea } from 'ant-design-vue';
const props = defineProps({ const props = defineProps({
content: { content: {
//
type: String, type: String,
default: '', default: '',
}, }, //
isWriting: { isWriting: {
//
type: Boolean, type: Boolean,
default: false, default: false,
}, }, //
}); });
const emits = defineEmits(['update:content', 'stopStream']); const emits = defineEmits(['update:content', 'stopStream']);
const { copied, copy } = useClipboard(); const { copied, copy } = useClipboard();
@@ -58,7 +56,6 @@ watch(copied, (val) => {
<template #title> <template #title>
<h3 class="m-0 flex shrink-0 items-center justify-between px-7"> <h3 class="m-0 flex shrink-0 items-center justify-between px-7">
<span>预览</span> <span>预览</span>
<!-- 展示在右上角 -->
<Button <Button
type="primary" type="primary"
v-show="showCopy" v-show="showCopy"
@@ -75,12 +72,11 @@ watch(copied, (val) => {
class="hide-scroll-bar box-border h-full overflow-y-auto" class="hide-scroll-bar box-border h-full overflow-y-auto"
> >
<div <div
class="bg-card relative box-border min-h-full w-full flex-grow p-3 sm:p-7" class="bg-card relative box-border min-h-full w-full flex-grow p-2 sm:p-5"
> >
<!-- 终止生成内容的按钮 -->
<Button <Button
v-show="isWriting" v-show="isWriting"
class="absolute bottom-2 left-1/2 z-40 flex -translate-x-1/2 sm:bottom-5" class="absolute bottom-1 left-1/2 z-40 flex -translate-x-1/2 sm:bottom-2"
@click="emits('stopStream')" @click="emits('stopStream')"
size="small" size="small"
> >
@@ -94,7 +90,7 @@ watch(copied, (val) => {
<Textarea <Textarea
id="inputId" id="inputId"
v-model:value="compContent" v-model:value="compContent"
auto-size :auto-size="true"
:bordered="false" :bordered="false"
placeholder="生成的内容……" placeholder="生成的内容……"
/> />
@@ -130,7 +126,7 @@ watch(copied, (val) => {
} }
} }
// markmaptool // markmap tool
:deep(.markmap) { :deep(.markmap) {
width: 100%; width: 100%;
} }

View File

@@ -1,6 +1,5 @@
<!-- 标签选项 --> <!-- 标签选项 -->
<script setup lang="ts"> <script setup lang="ts">
// TODO @gjd modules
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
[k: string]: any; [k: string]: any;

View File

@@ -8,12 +8,9 @@ import { getDictOptions } from '@vben/hooks';
import { getSimpleUserList } from '#/api/system/user'; import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils'; import { getRangePickerDefaultProps } from '#/utils';
/** 关联数据 */
let userList: SystemUserApi.User[] = []; let userList: SystemUserApi.User[] = [];
async function getUserData() { getSimpleUserList().then((data) => (userList = data));
userList = await getSimpleUserList();
}
getUserData();
/** 列表的搜索表单 */ /** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] { export function useGridFormSchema(): VbenFormSchema[] {
@@ -26,6 +23,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
api: getSimpleUserList, api: getSimpleUserList,
labelField: 'nickname', labelField: 'nickname',
valueField: 'id', valueField: 'id',
placeholder: '请选择用户',
allowClear: true,
}, },
}, },
{ {
@@ -34,6 +33,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'Select', component: 'Select',
componentProps: { componentProps: {
allowClear: true, allowClear: true,
placeholder: '请选择写作类型',
options: getDictOptions(DICT_TYPE.AI_WRITE_TYPE, 'number'), options: getDictOptions(DICT_TYPE.AI_WRITE_TYPE, 'number'),
}, },
}, },
@@ -43,7 +43,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'Select', component: 'Select',
componentProps: { componentProps: {
allowClear: true, allowClear: true,
options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'number'), placeholder: '请选择平台',
options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
}, },
}, },
{ {
@@ -78,7 +79,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{ {
field: 'type', field: 'type',
title: '写作类型', title: '写作类型',
minWidth: 100, minWidth: 120,
cellRender: { cellRender: {
name: 'CellDict', name: 'CellDict',
props: { type: DICT_TYPE.AI_WRITE_TYPE }, props: { type: DICT_TYPE.AI_WRITE_TYPE },

View File

@@ -17,22 +17,21 @@ function handleRefresh() {
gridApi.query(); gridApi.query();
} }
/** 删除 */ /** 删除写作记录 */
async function handleDelete(row: AiWriteApi.AiWritePageReq) { async function handleDelete(row: AiWriteApi.Write) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]), content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0, duration: 0,
}); });
try { try {
await deleteWrite(row.id as number); await deleteWrite(row.id!);
message.success({ message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
});
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();
} }
} }
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
@@ -54,12 +53,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<AiWriteApi.AiWritePageReq>, } as VxeTableGridOptions<AiWriteApi.Write>,
}); });
</script> </script>

View File

@@ -89,13 +89,11 @@ async function handleTransform(): Promise<boolean | undefined> {
content: '确定将该线索转化为客户吗?', content: '确定将该线索转化为客户吗?',
}) })
.then(async () => { .then(async () => {
const res = await transformClue(clueId.value); // 转化为客户
if (res) { await transformClue(clueId.value);
message.success('转化客户成功'); // 提示并返回成功
resolve(true); message.success('转化客户成功');
} else { resolve(true);
reject(new Error('转化失败'));
}
}) })
.catch(() => { .catch(() => {
reject(new Error('取消操作')); reject(new Error('取消操作'));

View File

@@ -107,13 +107,11 @@ function handleLock(lockStatus: boolean): Promise<boolean | undefined> {
content: `确定锁定客户【${customer.value.name}】吗?`, content: `确定锁定客户【${customer.value.name}】吗?`,
}) })
.then(async () => { .then(async () => {
const res = await lockCustomer(customerId.value, lockStatus); // 锁定客户
if (res) { await lockCustomer(customerId.value, lockStatus);
message.success(lockStatus ? '锁定客户成功' : '解锁客户成功'); // 提示并返回成功
resolve(true); message.success(lockStatus ? '锁定客户成功' : '解锁客户成功');
} else { resolve(true);
reject(new Error(lockStatus ? '锁定客户失败' : '解锁客户失败'));
}
}) })
.catch(() => { .catch(() => {
reject(new Error('取消操作')); reject(new Error('取消操作'));
@@ -128,13 +126,11 @@ function handleReceive(): Promise<boolean | undefined> {
content: `确定领取客户【${customer.value.name}】吗?`, content: `确定领取客户【${customer.value.name}】吗?`,
}) })
.then(async () => { .then(async () => {
const res = await receiveCustomer([customerId.value]); // 领取客户
if (res) { await receiveCustomer([customerId.value]);
message.success('领取客户成功'); // 提示并返回成功
resolve(true); message.success('领取客户成功');
} else { resolve(true);
reject(new Error('领取客户失败'));
}
}) })
.catch(() => { .catch(() => {
reject(new Error('取消操作')); reject(new Error('取消操作'));
@@ -154,13 +150,11 @@ function handlePutPool(): Promise<boolean | undefined> {
content: `确定将客户【${customer.value.name}】放入公海吗?`, content: `确定将客户【${customer.value.name}】放入公海吗?`,
}) })
.then(async () => { .then(async () => {
const res = await putCustomerPool(customerId.value); // 放入公海
if (res) { await putCustomerPool(customerId.value);
message.success('放入公海成功'); // 提示并返回成功
resolve(true); message.success('放入公海成功');
} else { resolve(true);
reject(new Error('放入公海失败'));
}
}) })
.catch(() => { .catch(() => {
reject(new Error('取消操作')); reject(new Error('取消操作'));
@@ -176,16 +170,11 @@ async function handleUpdateDealStatus(): Promise<boolean | undefined> {
content: `确定更新成交状态为【${dealStatus ? '已成交' : '未成交'}】吗?`, content: `确定更新成交状态为【${dealStatus ? '已成交' : '未成交'}】吗?`,
}) })
.then(async () => { .then(async () => {
const res = await updateCustomerDealStatus( // 更新成交状态
customerId.value, await updateCustomerDealStatus(customerId.value, dealStatus);
dealStatus, // 提示并返回成功
); message.success('更新成交状态成功');
if (res) { resolve(true);
message.success('更新成交状态成功');
resolve(true);
} else {
reject(new Error('更新成交状态失败'));
}
}) })
.catch(() => { .catch(() => {
reject(new Error('取消操作')); reject(new Error('取消操作'));

View File

@@ -73,6 +73,7 @@ async function handleDefaultStatusChange(
.then(async () => { .then(async () => {
// 更新默认状态 // 更新默认状态
await updateAccountDefaultStatus(row.id!, newStatus); await updateAccountDefaultStatus(row.id!, newStatus);
// 提示并返回成功
message.success(`${text}默认成功`); message.success(`${text}默认成功`);
resolve(true); resolve(true);
}) })

View File

@@ -71,6 +71,7 @@ async function handleDefaultStatusChange(
.then(async () => { .then(async () => {
// 更新默认状态 // 更新默认状态
await updateWarehouseDefaultStatus(row.id!, newStatus); await updateWarehouseDefaultStatus(row.id!, newStatus);
// 提示并返回成功
message.success(`${text}默认成功`); message.success(`${text}默认成功`);
resolve(true); resolve(true);
}) })

View File

@@ -70,17 +70,13 @@ async function handleStatusChange(
}) })
.then(async () => { .then(async () => {
// 更新状态 // 更新状态
const res = await updateCommentVisible({ await updateCommentVisible({
id: row.id!, id: row.id!,
visible: newStatus, visible: newStatus,
}); });
if (res) { // 提示并返回成功
// 提示并返回成功 message.success(`${text}成功`);
message.success(`${text}成功`); resolve(true);
resolve(true);
} else {
reject(new Error($t('ui.actionMessage.operationFailed')));
}
}) })
.catch(() => { .catch(() => {
reject(new Error('取消操作')); reject(new Error('取消操作'));

View File

@@ -115,17 +115,13 @@ async function handleStatusChange(
}) })
.then(async () => { .then(async () => {
// 更新状态 // 更新状态
const res = await updateStatus({ await updateStatus({
id: row.id!, id: row.id!,
status: newStatus, status: newStatus,
}); });
if (res) { // 提示并返回成功
// 提示并返回成功 message.success(`${text}成功`);
message.success(`${text}成功`); resolve(true);
resolve(true);
} else {
reject(new Error($t('ui.actionMessage.operationFailed')));
}
}) })
.catch(() => { .catch(() => {
reject(new Error('取消操作')); reject(new Error('取消操作'));

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { PREDEFINE_COLORS } from '@vben/constants'; // import { PREDEFINE_COLORS } from '@vben/constants';
import { Input, InputGroup } from 'ant-design-vue'; import { Input, InputGroup } from 'ant-design-vue';
@@ -29,7 +29,9 @@ const color = computed({
<template> <template>
<InputGroup compact> <InputGroup compact>
<!-- TODO 芋艿后续在处理antd 不支持该组件
<ColorPicker v-model:value="color" :presets="PREDEFINE_COLORS" /> <ColorPicker v-model:value="color" :presets="PREDEFINE_COLORS" />
-->
<Input v-model:value="color" class="flex-1" /> <Input v-model:value="color" class="flex-1" />
</InputGroup> </InputGroup>
</template> </template>

View File

@@ -5,6 +5,8 @@ import { ref } from 'vue';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Image } from 'ant-design-vue';
/** 弹窗广告 */ /** 弹窗广告 */
defineOptions({ name: 'Popover' }); defineOptions({ name: 'Popover' });
// 定义属性 // 定义属性

View File

@@ -5,7 +5,7 @@ import { ref } from 'vue';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { message } from 'ant-design-vue'; import { Image, message } from 'ant-design-vue';
/** 悬浮按钮 */ /** 悬浮按钮 */
defineOptions({ name: 'FloatingActionButton' }); defineOptions({ name: 'FloatingActionButton' });

View File

@@ -5,6 +5,8 @@ import { ref } from 'vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Button, Form, FormItem, Typography } from 'ant-design-vue';
import UploadImg from '#/components/upload/image-upload.vue'; import UploadImg from '#/components/upload/image-upload.vue';
import ComponentContainerProperty from '../../component-container-property.vue'; import ComponentContainerProperty from '../../component-container-property.vue';
@@ -28,8 +30,8 @@ const handleOpenEditDialog = () => {
<template> <template>
<ComponentContainerProperty v-model="formData.style"> <ComponentContainerProperty v-model="formData.style">
<!-- 表单 --> <!-- 表单 -->
<ElForm label-width="80px" :model="formData" class="mt-2"> <Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }" :model="formData" class="mt-2">
<ElFormItem label="上传图片" prop="imgUrl"> <FormItem label="上传图片" prop="imgUrl">
<UploadImg <UploadImg
v-model="formData.imgUrl" v-model="formData.imgUrl"
height="50px" height="50px"
@@ -38,15 +40,15 @@ const handleOpenEditDialog = () => {
:show-description="false" :show-description="false"
> >
<template #tip> <template #tip>
<ElText type="info" size="small"> 推荐宽度 750</ElText> <Typography.Text type="secondary" class="text-xs"> 推荐宽度 750</Typography.Text>
</template> </template>
</UploadImg> </UploadImg>
</ElFormItem> </FormItem>
</ElForm> </Form>
<ElButton type="primary" plain class="w-full" @click="handleOpenEditDialog"> <Button type="primary" class="w-full" @click="handleOpenEditDialog">
设置热区 设置热区
</ElButton> </Button>
</ComponentContainerProperty> </ComponentContainerProperty>
<!-- 热区编辑对话框 --> <!-- 热区编辑对话框 -->
<HotZoneEditDialog <HotZoneEditDialog

View File

@@ -3,6 +3,8 @@ import type { ImageBarProperty } from './config';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Form, FormItem } from 'ant-design-vue';
import UploadImg from '#/components/upload/image-upload.vue'; import UploadImg from '#/components/upload/image-upload.vue';
import { AppLinkInput } from '#/views/mall/promotion/components'; import { AppLinkInput } from '#/views/mall/promotion/components';
@@ -18,8 +20,8 @@ const formData = useVModel(props, 'modelValue', emit);
<template> <template>
<ComponentContainerProperty v-model="formData.style"> <ComponentContainerProperty v-model="formData.style">
<ElForm label-width="80px" :model="formData"> <Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }" :model="formData">
<ElFormItem label="上传图片" prop="imgUrl"> <FormItem label="上传图片" prop="imgUrl">
<UploadImg <UploadImg
v-model="formData.imgUrl" v-model="formData.imgUrl"
draggable="false" draggable="false"
@@ -30,11 +32,11 @@ const formData = useVModel(props, 'modelValue', emit);
> >
<template #tip> 建议宽度750 </template> <template #tip> 建议宽度750 </template>
</UploadImg> </UploadImg>
</ElFormItem> </FormItem>
<ElFormItem label="链接" prop="url"> <FormItem label="链接" prop="url">
<AppLinkInput v-model="formData.url" /> <AppLinkInput v-model="formData.url" />
</ElFormItem> </FormItem>
</ElForm> </Form>
</ComponentContainerProperty> </ComponentContainerProperty>
</template> </template>

View File

@@ -3,12 +3,13 @@ import type { MenuGridProperty } from './config';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { import {
ElCard, Card,
ElForm, Form,
ElFormItem, FormItem,
ElRadio, Radio,
ElRadioGroup, RadioGroup,
ElSwitch, Switch,
} from 'ant-design-vue';
import ComponentContainerProperty from '../../component-container-property.vue'; import ComponentContainerProperty from '../../component-container-property.vue';
import UploadImg from '#/components/upload/image-upload.vue'; import UploadImg from '#/components/upload/image-upload.vue';
@@ -27,21 +28,21 @@ const formData = useVModel(props, 'modelValue', emit);
<template> <template>
<ComponentContainerProperty v-model="formData.style"> <ComponentContainerProperty v-model="formData.style">
<!-- 表单 --> <!-- 表单 -->
<ElForm label-width="80px" :model="formData" class="mt-2"> <Form label-width="80px" :model="formData" class="mt-2">
<ElFormItem label="每行数量" prop="column"> <FormItem label="每行数量" prop="column">
<ElRadioGroup v-model="formData.column"> <RadioGroup v-model="formData.column">
<ElRadio :value="3">3</ElRadio> <Radio :value="3">3</Radio>
<ElRadio :value="4">4</ElRadio> <Radio :value="4">4</Radio>
</ElRadioGroup> </RadioGroup>
</ElFormItem> </FormItem>
<ElCard header="菜单设置" class="property-group" shadow="never"> <Card header="菜单设置" class="property-group" shadow="never">
<Draggable <Draggable
v-model="formData.list" v-model="formData.list"
:empty-item="EMPTY_MENU_GRID_ITEM_PROPERTY" :empty-item="EMPTY_MENU_GRID_ITEM_PROPERTY"
> >
<template #default="{ element }"> <template #default="{ element }">
<ElFormItem label="图标" prop="iconUrl"> <FormItem label="图标" prop="iconUrl">
<UploadImg <UploadImg
v-model="element.iconUrl" v-model="element.iconUrl"
height="80px" height="80px"
@@ -50,40 +51,40 @@ const formData = useVModel(props, 'modelValue', emit);
> >
<template #tip> 建议尺寸44 * 44 </template> <template #tip> 建议尺寸44 * 44 </template>
</UploadImg> </UploadImg>
</ElFormItem> </FormItem>
<ElFormItem label="标题" prop="title"> <FormItem label="标题" prop="title">
<InputWithColor <InputWithColor
v-model="element.title" v-model="element.title"
v-model:color="element.titleColor" v-model:color="element.titleColor"
/> />
</ElFormItem> </FormItem>
<ElFormItem label="副标题" prop="subtitle"> <FormItem label="副标题" prop="subtitle">
<InputWithColor <InputWithColor
v-model="element.subtitle" v-model="element.subtitle"
v-model:color="element.subtitleColor" v-model:color="element.subtitleColor"
/> />
</ElFormItem> </FormItem>
<ElFormItem label="链接" prop="url"> <FormItem label="链接" prop="url">
<AppLinkInput v-model="element.url" /> <AppLinkInput v-model="element.url" />
</ElFormItem> </FormItem>
<ElFormItem label="显示角标" prop="badge.show"> <FormItem label="显示角标" prop="badge.show">
<ElSwitch v-model="element.badge.show" /> <Switch v-model="element.badge.show" />
</ElFormItem> </FormItem>
<template v-if="element.badge.show"> <template v-if="element.badge.show">
<ElFormItem label="角标内容" prop="badge.text"> <FormItem label="角标内容" prop="badge.text">
<InputWithColor <InputWithColor
v-model="element.badge.text" v-model="element.badge.text"
v-model:color="element.badge.textColor" v-model:color="element.badge.textColor"
/> />
</ElFormItem> </FormItem>
<ElFormItem label="背景颜色" prop="badge.bgColor"> <FormItem label="背景颜色" prop="badge.bgColor">
<ColorInput v-model="element.badge.bgColor" /> <ColorInput v-model="element.badge.bgColor" />
</ElFormItem> </FormItem>
</template> </template>
</template> </template>
</Draggable> </Draggable>
</ElCard> </Card>
</ElForm> </Form>
</ComponentContainerProperty> </ComponentContainerProperty>
</template> </template>

View File

@@ -3,6 +3,8 @@ import type { MenuListProperty } from './config';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Image } from 'ant-design-vue';
/** 列表导航 */ /** 列表导航 */
defineOptions({ name: 'MenuList' }); defineOptions({ name: 'MenuList' });
defineProps<{ property: MenuListProperty }>(); defineProps<{ property: MenuListProperty }>();

View File

@@ -1,4 +1,145 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { NavigationBarCellProperty } from '../config';
import type { Rect } from '#/views/mall/promotion/components/magic-cube-editor/util';
import { computed, ref } from 'vue';
import { useVModel } from '@vueuse/core';
import {
FormItem,
Input,
Radio,
RadioGroup,
Slider,
} from 'ant-design-vue';
import appNavBarMp from '#/assets/imgs/diy/app-nav-bar-mp.png';
import UploadImg from '#/components/upload/image-upload.vue';
import {
AppLinkInput,
ColorInput,
MagicCubeEditor,
} from '#/views/mall/promotion/components';
// 导航栏属性面板
defineOptions({ name: 'NavigationBarCellProperty' });
const props = defineProps({
isMp: {
type: Boolean,
default: true,
},
modelValue: {
type: Array as () => NavigationBarCellProperty[],
default: () => [],
},
});
const emit = defineEmits(['update:modelValue']);
const cellList = useVModel(props, 'modelValue', emit);
// 单元格数量小程序6个右侧胶囊按钮占了2个其它平台8个
const cellCount = computed(() => (props.isMp ? 6 : 8));
// 转换为Rect格式的数据
const rectList = computed<Rect[]>(() => {
return cellList.value.map((cell) => ({
left: cell.left,
top: cell.top,
width: cell.width,
height: cell.height,
right: cell.left + cell.width,
bottom: cell.top + cell.height,
}));
});
// 选中的热区
const selectedHotAreaIndex = ref(0);
const handleHotAreaSelected = (
cellValue: NavigationBarCellProperty,
index: number,
) => {
selectedHotAreaIndex.value = index;
if (!cellValue.type) {
cellValue.type = 'text';
cellValue.textColor = '#111111';
}
};
</script>
<template>
<div class="h-40px flex items-center justify-center">
<MagicCubeEditor
v-model="rectList"
:cols="cellCount"
:cube-size="38"
:rows="1"
class="m-b-16px"
@hot-area-selected="handleHotAreaSelected"
/>
<img
v-if="isMp"
alt=""
style="width: 76px; height: 30px"
:src="appNavBarMp"
/>
</div>
<template v-for="(cell, cellIndex) in cellList" :key="cellIndex">
<template v-if="selectedHotAreaIndex === Number(cellIndex)">
<FormItem :label="`类型`">
<RadioGroup v-model:value="cell.type">
<Radio value="text">文字</Radio>
<Radio value="image">图片</Radio>
<Radio value="search">搜索框</Radio>
</RadioGroup>
</FormItem>
<!-- 1. 文字 -->
<template v-if="cell.type === 'text'">
<FormItem :label="`内容`">
<Input v-model:value="cell!.text" :maxlength="10" show-count />
</FormItem>
<FormItem :label="`颜色`">
<ColorInput v-model="cell!.textColor" />
</FormItem>
<FormItem :label="`链接`">
<AppLinkInput v-model="cell.url" />
</FormItem>
</template>
<!-- 2. 图片 -->
<template v-else-if="cell.type === 'image'">
<FormItem :label="`图片`">
<UploadImg
v-model="cell.imgUrl"
:limit="1"
height="56px"
width="56px"
:show-description="false"
>
<template #tip>建议尺寸 56*56</template>
</UploadImg>
</FormItem>
<FormItem :label="`链接`">
<AppLinkInput v-model="cell.url" />
</FormItem>
</template>
<!-- 3. 搜索框 -->
<template v-else>
<FormItem :label="`提示文字`">
<Input v-model:value="cell.placeholder" :maxlength="10" show-count />
</FormItem>
<FormItem :label="`圆角`">
<Slider
v-model:value="cell.borderRadius"
:max="100"
:min="0"
/>
</FormItem>
</template>
</template>
</template>
</template>
<style lang="scss" scoped></style>

View File

@@ -1,12 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NoticeBarProperty } from './config'; import type { NoticeBarProperty } from './config';
import { ref } from 'vue';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Divider, Image } from 'ant-design-vue';
/** 公告栏 */ /** 公告栏 */
defineOptions({ name: 'NoticeBar' }); defineOptions({ name: 'NoticeBar' });
defineProps<{ property: NoticeBarProperty }>(); const props = defineProps<{ property: NoticeBarProperty }>();
// 自动轮播
const activeIndex = ref(0);
setInterval(() => {
const contents = props.property.contents || [];
activeIndex.value = (activeIndex.value + 1) % (contents.length || 1);
}, 3000);
</script> </script>
<template> <template>
@@ -17,18 +28,11 @@ defineProps<{ property: NoticeBarProperty }>();
color: property.textColor, color: property.textColor,
}" }"
> >
<ElImage :src="property.iconUrl" class="h-[18px]" /> <Image :src="property.iconUrl" class="h-[18px]" :preview="false" />
<ElDivider direction="vertical" /> <Divider type="vertical" />
<ElCarousel <div class="flex-1 pr-2 h-6 truncate leading-6">
height="24px" {{ property.contents?.[activeIndex]?.text }}
direction="vertical" </div>
:autoplay="true"
class="flex-1 pr-2"
>
<ElCarouselItem v-for="(item, index) in property.contents" :key="index">
<div class="h-6 truncate leading-6">{{ item.text }}</div>
</ElCarouselItem>
</ElCarousel>
<IconifyIcon icon="ep:arrow-right" /> <IconifyIcon icon="ep:arrow-right" />
</div> </div>
</template> </template>

View File

@@ -3,6 +3,8 @@ import type { PageConfigProperty } from './config';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Form, FormItem, Textarea } from 'ant-design-vue';
import UploadImg from '#/components/upload/image-upload.vue'; import UploadImg from '#/components/upload/image-upload.vue';
import { ColorInput } from '#/views/mall/promotion/components'; import { ColorInput } from '#/views/mall/promotion/components';
@@ -22,8 +24,8 @@ const formData = useVModel(props, 'modelValue', emit);
<Form <Form
:model="formData" :model="formData"
:rules="rules" :rules="rules"
label-col="{ span: 6 }" :label-col="{ span: 6 }"
wrapper-col="{ span: 18 }" :wrapper-col="{ span: 18 }"
> >
<FormItem label="页面描述" name="description"> <FormItem label="页面描述" name="description">
<Textarea <Textarea

View File

@@ -7,6 +7,8 @@ import { ref, watch } from 'vue';
import { fenToYuan } from '@vben/utils'; import { fenToYuan } from '@vben/utils';
import { Image } from 'ant-design-vue';
import * as ProductSpuApi from '#/api/mall/product/spu'; import * as ProductSpuApi from '#/api/mall/product/spu';
/** 商品卡片 */ /** 商品卡片 */

View File

@@ -5,6 +5,18 @@ import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import {
Card,
Checkbox,
Form,
FormItem,
RadioButton,
RadioGroup,
Slider,
Switch,
Tooltip,
} from 'ant-design-vue';
import UploadImg from '#/components/upload/image-upload.vue'; import UploadImg from '#/components/upload/image-upload.vue';
import { InputWithColor as ColorInput } from '#/views/mall/promotion/components'; import { InputWithColor as ColorInput } from '#/views/mall/promotion/components';
@@ -22,48 +34,48 @@ const formData = useVModel(props, 'modelValue', emit);
<template> <template>
<ComponentContainerProperty v-model="formData.style"> <ComponentContainerProperty v-model="formData.style">
<ElForm label-width="80px" :model="formData"> <Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }" :model="formData">
<ElCard header="商品列表" class="property-group" shadow="never"> <Card title="商品列表" class="property-group" :bordered="false">
<!-- <SpuShowcase v-model="formData.spuIds" /> --> <!-- <SpuShowcase v-model="formData.spuIds" /> -->
</ElCard> </Card>
<ElCard header="商品样式" class="property-group" shadow="never"> <Card title="商品样式" class="property-group" :bordered="false">
<ElFormItem label="布局" prop="type"> <FormItem label="布局" prop="type">
<ElRadioGroup v-model="formData.layoutType"> <RadioGroup v-model:value="formData.layoutType">
<ElTooltip class="item" content="双列" placement="bottom"> <Tooltip title="双列" placement="bottom">
<ElRadioButton value="twoCol"> <RadioButton value="twoCol">
<IconifyIcon icon="fluent:text-column-two-24-filled" /> <IconifyIcon icon="fluent:text-column-two-24-filled" />
</ElRadioButton> </RadioButton>
</ElTooltip> </Tooltip>
<ElTooltip class="item" content="三列" placement="bottom"> <Tooltip title="三列" placement="bottom">
<ElRadioButton value="threeCol"> <RadioButton value="threeCol">
<IconifyIcon icon="fluent:text-column-three-24-filled" /> <IconifyIcon icon="fluent:text-column-three-24-filled" />
</ElRadioButton> </RadioButton>
</ElTooltip> </Tooltip>
<ElTooltip class="item" content="水平滑动" placement="bottom"> <Tooltip title="水平滑动" placement="bottom">
<ElRadioButton value="horizSwiper"> <RadioButton value="horizSwiper">
<IconifyIcon icon="system-uicons:carousel" /> <IconifyIcon icon="system-uicons:carousel" />
</ElRadioButton> </RadioButton>
</ElTooltip> </Tooltip>
</ElRadioGroup> </RadioGroup>
</ElFormItem> </FormItem>
<ElFormItem label="商品名称" prop="fields.name.show"> <FormItem label="商品名称" prop="fields.name.show">
<div class="flex gap-2"> <div class="flex gap-2">
<ColorInput v-model="formData.fields.name.color" /> <ColorInput v-model="formData.fields.name.color" />
<ElCheckbox v-model="formData.fields.name.show" /> <Checkbox v-model:checked="formData.fields.name.show" />
</div> </div>
</ElFormItem> </FormItem>
<ElFormItem label="商品价格" prop="fields.price.show"> <FormItem label="商品价格" prop="fields.price.show">
<div class="flex gap-2"> <div class="flex gap-2">
<ColorInput v-model="formData.fields.price.color" /> <ColorInput v-model="formData.fields.price.color" />
<ElCheckbox v-model="formData.fields.price.show" /> <Checkbox v-model:checked="formData.fields.price.show" />
</div> </div>
</ElFormItem> </FormItem>
</ElCard> </Card>
<ElCard header="角标" class="property-group" shadow="never"> <Card title="角标" class="property-group" :bordered="false">
<ElFormItem label="角标" prop="badge.show"> <FormItem label="角标" prop="badge.show">
<ElSwitch v-model="formData.badge.show" /> <Switch v-model:checked="formData.badge.show" />
</ElFormItem> </FormItem>
<ElFormItem label="角标" prop="badge.imgUrl" v-if="formData.badge.show"> <FormItem label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
<UploadImg <UploadImg
v-model="formData.badge.imgUrl" v-model="formData.badge.imgUrl"
height="44px" height="44px"
@@ -72,41 +84,32 @@ const formData = useVModel(props, 'modelValue', emit);
> >
<template #tip> 建议尺寸36 * 22 </template> <template #tip> 建议尺寸36 * 22 </template>
</UploadImg> </UploadImg>
</ElFormItem> </FormItem>
</ElCard> </Card>
<ElCard header="商品样式" class="property-group" shadow="never"> <Card title="商品样式" class="property-group" :bordered="false">
<ElFormItem label="上圆角" prop="borderRadiusTop"> <FormItem label="上圆角" prop="borderRadiusTop">
<ElSlider <Slider
v-model="formData.borderRadiusTop" v-model:value="formData.borderRadiusTop"
:max="100" :max="100"
:min="0" :min="0"
show-input
input-size="small"
:show-input-controls="false"
/> />
</ElFormItem> </FormItem>
<ElFormItem label="下圆角" prop="borderRadiusBottom"> <FormItem label="下圆角" prop="borderRadiusBottom">
<ElSlider <Slider
v-model="formData.borderRadiusBottom" v-model:value="formData.borderRadiusBottom"
:max="100" :max="100"
:min="0" :min="0"
show-input
input-size="small"
:show-input-controls="false"
/> />
</ElFormItem> </FormItem>
<ElFormItem label="间隔" prop="space"> <FormItem label="间隔" prop="space">
<ElSlider <Slider
v-model="formData.space" v-model:value="formData.space"
:max="100" :max="100"
:min="0" :min="0"
show-input
input-size="small"
:show-input-controls="false"
/> />
</ElFormItem> </FormItem>
</ElCard> </Card>
</ElForm> </Form>
</ComponentContainerProperty> </ComponentContainerProperty>
</template> </template>

View File

@@ -7,6 +7,8 @@ import { onMounted, ref } from 'vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Form, FormItem, Select } from 'ant-design-vue';
import * as ArticleApi from '#/api/mall/promotion/article/index'; import * as ArticleApi from '#/api/mall/promotion/article/index';
import ComponentContainerProperty from '../../component-container-property.vue'; import ComponentContainerProperty from '../../component-container-property.vue';
@@ -42,26 +44,19 @@ onMounted(() => {
<template> <template>
<ComponentContainerProperty v-model="formData.style"> <ComponentContainerProperty v-model="formData.style">
<ElForm label-width="40px" :model="formData"> <Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }" :model="formData">
<ElFormItem label="文章" prop="id"> <FormItem label="文章" prop="id">
<ElSelect <Select
v-model="formData.id" v-model:value="formData.id"
placeholder="请选择文章" placeholder="请选择文章"
class="w-full" class="w-full"
filterable filterable
remote
:remote-method="queryArticleList"
:loading="loading" :loading="loading"
> :options="articles.map((item) => ({ label: item.title, value: item.id }))"
<ElOption @search="queryArticleList"
v-for="article in articles" />
:key="article.id" </FormItem>
:label="article.title" </Form>
:value="article.id"
/>
</ElSelect>
</ElFormItem>
</ElForm>
</ComponentContainerProperty> </ComponentContainerProperty>
</template> </template>

View File

@@ -8,6 +8,8 @@ import { ref, watch } from 'vue';
import { fenToYuan } from '@vben/utils'; import { fenToYuan } from '@vben/utils';
import { Image } from 'ant-design-vue';
import * as ProductSpuApi from '#/api/mall/product/spu'; import * as ProductSpuApi from '#/api/mall/product/spu';
import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity'; import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity';

View File

@@ -10,6 +10,20 @@ import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import {
Card,
Checkbox,
Form,
FormItem,
Input,
Radio,
RadioButton,
RadioGroup,
Slider,
Switch,
Tooltip,
} from 'ant-design-vue';
import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity'; import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity';
import UploadImg from '#/components/upload/image-upload.vue'; import UploadImg from '#/components/upload/image-upload.vue';
import CombinationShowcase from '#/views/mall/promotion/combination/components/combination-showcase.vue'; import CombinationShowcase from '#/views/mall/promotion/combination/components/combination-showcase.vue';
@@ -35,102 +49,97 @@ onMounted(async () => {
<template> <template>
<ComponentContainerProperty v-model="formData.style"> <ComponentContainerProperty v-model="formData.style">
<ElForm label-width="80px" :model="formData"> <Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }" :model="formData">
<ElCard header="拼团活动" class="property-group" shadow="never"> <Card title="拼团活动" class="property-group" :bordered="false">
<CombinationShowcase v-model="formData.activityIds" /> <CombinationShowcase v-model="formData.activityIds" />
</ElCard> </Card>
<ElCard header="商品样式" class="property-group" shadow="never"> <Card title="商品样式" class="property-group" :bordered="false">
<ElFormItem label="布局" prop="type"> <FormItem label="布局" prop="type">
<ElRadioGroup v-model="formData.layoutType"> <RadioGroup v-model:value="formData.layoutType">
<ElTooltip class="item" content="单列大图" placement="bottom"> <Tooltip title="单列大图" placement="bottom">
<ElRadioButton value="oneColBigImg"> <RadioButton value="oneColBigImg">
<IconifyIcon icon="fluent:text-column-one-24-filled" /> <IconifyIcon icon="fluent:text-column-one-24-filled" />
</ElRadioButton> </RadioButton>
</ElTooltip> </Tooltip>
<ElTooltip class="item" content="单列小图" placement="bottom"> <Tooltip title="单列小图" placement="bottom">
<ElRadioButton value="oneColSmallImg"> <RadioButton value="oneColSmallImg">
<IconifyIcon icon="fluent:text-column-two-left-24-filled" /> <IconifyIcon icon="fluent:text-column-two-left-24-filled" />
</ElRadioButton> </RadioButton>
</ElTooltip> </Tooltip>
<ElTooltip class="item" content="双列" placement="bottom"> <Tooltip title="双列" placement="bottom">
<ElRadioButton value="twoCol"> <RadioButton value="twoCol">
<IconifyIcon icon="fluent:text-column-two-24-filled" /> <IconifyIcon icon="fluent:text-column-two-24-filled" />
</ElRadioButton> </RadioButton>
</ElTooltip> </Tooltip>
<!--<el-tooltip class="item" content="三列" placement="bottom"> </RadioGroup>
<el-radio-button value="threeCol"> </FormItem>
<IconifyIcon icon="fluent:text-column-three-24-filled" /> <FormItem label="商品名称" prop="fields.name.show">
</ElRadioButton>
</ElTooltip>-->
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="商品名称" prop="fields.name.show">
<div class="flex gap-2"> <div class="flex gap-2">
<ColorInput v-model="formData.fields.name.color" /> <ColorInput v-model="formData.fields.name.color" />
<ElCheckbox v-model="formData.fields.name.show" /> <Checkbox v-model:checked="formData.fields.name.show" />
</div> </div>
</ElFormItem> </FormItem>
<ElFormItem label="商品简介" prop="fields.introduction.show"> <FormItem label="商品简介" prop="fields.introduction.show">
<div class="flex gap-2"> <div class="flex gap-2">
<ColorInput v-model="formData.fields.introduction.color" /> <ColorInput v-model="formData.fields.introduction.color" />
<ElCheckbox v-model="formData.fields.introduction.show" /> <Checkbox v-model:checked="formData.fields.introduction.show" />
</div> </div>
</ElFormItem> </FormItem>
<ElFormItem label="商品价格" prop="fields.price.show"> <FormItem label="商品价格" prop="fields.price.show">
<div class="flex gap-2"> <div class="flex gap-2">
<ColorInput v-model="formData.fields.price.color" /> <ColorInput v-model="formData.fields.price.color" />
<ElCheckbox v-model="formData.fields.price.show" /> <Checkbox v-model:checked="formData.fields.price.show" />
</div> </div>
</ElFormItem> </FormItem>
<ElFormItem label="市场价" prop="fields.marketPrice.show"> <FormItem label="市场价" prop="fields.marketPrice.show">
<div class="flex gap-2"> <div class="flex gap-2">
<ColorInput v-model="formData.fields.marketPrice.color" /> <ColorInput v-model="formData.fields.marketPrice.color" />
<ElCheckbox v-model="formData.fields.marketPrice.show" /> <Checkbox v-model:checked="formData.fields.marketPrice.show" />
</div> </div>
</ElFormItem> </FormItem>
<ElFormItem label="商品销量" prop="fields.salesCount.show"> <FormItem label="商品销量" prop="fields.salesCount.show">
<div class="flex gap-2"> <div class="flex gap-2">
<ColorInput v-model="formData.fields.salesCount.color" /> <ColorInput v-model="formData.fields.salesCount.color" />
<ElCheckbox v-model="formData.fields.salesCount.show" /> <Checkbox v-model:checked="formData.fields.salesCount.show" />
</div> </div>
</ElFormItem> </FormItem>
<ElFormItem label="商品库存" prop="fields.stock.show"> <FormItem label="商品库存" prop="fields.stock.show">
<div class="flex gap-2"> <div class="flex gap-2">
<ColorInput v-model="formData.fields.stock.color" /> <ColorInput v-model="formData.fields.stock.color" />
<ElCheckbox v-model="formData.fields.stock.show" /> <Checkbox v-model:checked="formData.fields.stock.show" />
</div> </div>
</ElFormItem> </FormItem>
</ElCard> </Card>
<ElCard header="角标" class="property-group" shadow="never"> <Card title="角标" class="property-group" :bordered="false">
<ElFormItem label="角标" prop="badge.show"> <FormItem label="角标" prop="badge.show">
<ElSwitch v-model="formData.badge.show" /> <Switch v-model:checked="formData.badge.show" />
</ElFormItem> </FormItem>
<ElFormItem label="角标" prop="badge.imgUrl" v-if="formData.badge.show"> <FormItem label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
<template #tip> 建议尺寸36 * 22</template> <template #tip> 建议尺寸36 * 22</template>
</UploadImg> </UploadImg>
</ElFormItem> </FormItem>
</ElCard> </Card>
<ElCard header="按钮" class="property-group" shadow="never"> <Card title="按钮" class="property-group" :bordered="false">
<ElFormItem label="按钮类型" prop="btnBuy.type"> <FormItem label="按钮类型" prop="btnBuy.type">
<ElRadioGroup v-model="formData.btnBuy.type"> <RadioGroup v-model:value="formData.btnBuy.type">
<ElRadioButton value="text">文字</ElRadioButton> <RadioButton value="text">文字</RadioButton>
<ElRadioButton value="img">图片</ElRadioButton> <RadioButton value="img">图片</RadioButton>
</ElRadioGroup> </RadioGroup>
</ElFormItem> </FormItem>
<template v-if="formData.btnBuy.type === 'text'"> <template v-if="formData.btnBuy.type === 'text'">
<ElFormItem label="按钮文字" prop="btnBuy.text"> <FormItem label="按钮文字" prop="btnBuy.text">
<ElInput v-model="formData.btnBuy.text" /> <Input v-model:value="formData.btnBuy.text" />
</ElFormItem> </FormItem>
<ElFormItem label="左侧背景" prop="btnBuy.bgBeginColor"> <FormItem label="左侧背景" prop="btnBuy.bgBeginColor">
<ColorInput v-model="formData.btnBuy.bgBeginColor" /> <ColorInput v-model="formData.btnBuy.bgBeginColor" />
</ElFormItem> </FormItem>
<ElFormItem label="右侧背景" prop="btnBuy.bgEndColor"> <FormItem label="右侧背景" prop="btnBuy.bgEndColor">
<ColorInput v-model="formData.btnBuy.bgEndColor" /> <ColorInput v-model="formData.btnBuy.bgEndColor" />
</ElFormItem> </FormItem>
</template> </template>
<template v-else> <template v-else>
<ElFormItem label="图片" prop="btnBuy.imgUrl"> <FormItem label="图片" prop="btnBuy.imgUrl">
<UploadImg <UploadImg
v-model="formData.btnBuy.imgUrl" v-model="formData.btnBuy.imgUrl"
height="56px" height="56px"
@@ -139,42 +148,33 @@ onMounted(async () => {
> >
<template #tip> 建议尺寸56 * 56</template> <template #tip> 建议尺寸56 * 56</template>
</UploadImg> </UploadImg>
</ElFormItem> </FormItem>
</template> </template>
</ElCard> </Card>
<ElCard header="商品样式" class="property-group" shadow="never"> <Card title="商品样式" class="property-group" :bordered="false">
<ElFormItem label="上圆角" prop="borderRadiusTop"> <FormItem label="上圆角" prop="borderRadiusTop">
<ElSlider <Slider
v-model="formData.borderRadiusTop" v-model:value="formData.borderRadiusTop"
:max="100" :max="100"
:min="0" :min="0"
show-input
input-size="small"
:show-input-controls="false"
/> />
</ElFormItem> </FormItem>
<ElFormItem label="下圆角" prop="borderRadiusBottom"> <FormItem label="下圆角" prop="borderRadiusBottom">
<ElSlider <Slider
v-model="formData.borderRadiusBottom" v-model:value="formData.borderRadiusBottom"
:max="100" :max="100"
:min="0" :min="0"
show-input
input-size="small"
:show-input-controls="false"
/> />
</ElFormItem> </FormItem>
<ElFormItem label="间隔" prop="space"> <FormItem label="间隔" prop="space">
<ElSlider <Slider
v-model="formData.space" v-model:value="formData.space"
:max="100" :max="100"
:min="0" :min="0"
show-input
input-size="small"
:show-input-controls="false"
/> />
</ElFormItem> </FormItem>
</ElCard> </Card>
</ElForm> </Form>
</ComponentContainerProperty> </ComponentContainerProperty>
</template> </template>

View File

@@ -7,6 +7,8 @@ import { ref, watch } from 'vue';
import { fenToYuan } from '@vben/utils'; import { fenToYuan } from '@vben/utils';
import { Image } from 'ant-design-vue';
import * as ProductSpuApi from '#/api/mall/product/spu'; import * as ProductSpuApi from '#/api/mall/product/spu';
import * as PointActivityApi from '#/api/mall/promotion/point'; import * as PointActivityApi from '#/api/mall/promotion/point';

View File

@@ -8,6 +8,8 @@ import { ref, watch } from 'vue';
import { fenToYuan } from '@vben/utils'; import { fenToYuan } from '@vben/utils';
import { Image } from 'ant-design-vue';
import * as ProductSpuApi from '#/api/mall/product/spu'; import * as ProductSpuApi from '#/api/mall/product/spu';
import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity'; import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity';

View File

@@ -3,6 +3,8 @@ import type { TabBarProperty } from './config';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Image } from 'ant-design-vue';
/** 页面底部导航栏 */ /** 页面底部导航栏 */
defineOptions({ name: 'TabBar' }); defineOptions({ name: 'TabBar' });

View File

@@ -5,6 +5,16 @@ import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import {
Form,
FormItem,
Input,
RadioButton,
RadioGroup,
Select,
SelectOption,
} from 'ant-design-vue';
import UploadImg from '#/components/upload/image-upload.vue'; import UploadImg from '#/components/upload/image-upload.vue';
import { import {
AppLinkInput, AppLinkInput,
@@ -35,7 +45,7 @@ const handleThemeChange = () => {
<template> <template>
<div class="tab-bar"> <div class="tab-bar">
<!-- 表单 --> <!-- 表单 -->
<Form :model="formData" label-col="{ span: 6 }" wrapper-col="{ span: 18 }"> <Form :model="formData" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="主题" name="theme"> <FormItem label="主题" name="theme">
<Select v-model:value="formData!.theme" @change="handleThemeChange"> <Select v-model:value="formData!.theme" @change="handleThemeChange">
<SelectOption <SelectOption
@@ -109,8 +119,8 @@ const handleThemeChange = () => {
<FormItem <FormItem
name="text" name="text"
label="文字" label="文字"
label-col="{ span: 4 }" :label-col="{ span: 4 }"
wrapper-col="{ span: 20 }" :wrapper-col="{ span: 20 }"
class="mb-2" class="mb-2"
> >
<Input v-model:value="element.text" placeholder="请输入文字" /> <Input v-model:value="element.text" placeholder="请输入文字" />
@@ -118,8 +128,8 @@ const handleThemeChange = () => {
<FormItem <FormItem
name="url" name="url"
label="链接" label="链接"
label-col="{ span: 4 }" :label-col="{ span: 4 }"
wrapper-col="{ span: 20 }" :wrapper-col="{ span: 20 }"
class="mb-0" class="mb-0"
> >
<AppLinkInput v-model="element.url" /> <AppLinkInput v-model="element.url" />

View File

@@ -3,6 +3,8 @@ import type { TitleBarProperty } from './config';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Image } from 'ant-design-vue';
/** 标题栏 */ /** 标题栏 */
defineOptions({ name: 'TitleBar' }); defineOptions({ name: 'TitleBar' });

View File

@@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VideoPlayerProperty } from './config'; import type { VideoPlayerProperty } from './config';
import { Image } from 'ant-design-vue';
/** 视频播放 */ /** 视频播放 */
defineOptions({ name: 'VideoPlayer' }); defineOptions({ name: 'VideoPlayer' });

View File

@@ -3,6 +3,8 @@ import type { VideoPlayerProperty } from './config';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Form, FormItem, Slider, Switch } from 'ant-design-vue';
import UploadFile from '#/components/upload/file-upload.vue'; import UploadFile from '#/components/upload/file-upload.vue';
import UploadImg from '#/components/upload/image-upload.vue'; import UploadImg from '#/components/upload/image-upload.vue';
@@ -23,7 +25,7 @@ const formData = useVModel(props, 'modelValue', emit);
<Slider v-model:value="formData.style.height" :max="500" :min="100" /> <Slider v-model:value="formData.style.height" :max="500" :min="100" />
</FormItem> </FormItem>
</template> </template>
<Form :model="formData" label-col="{ span: 6 }" wrapper-col="{ span: 18 }"> <Form :model="formData" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="上传视频" name="videoUrl"> <FormItem label="上传视频" name="videoUrl">
<UploadFile <UploadFile
v-model="formData.videoUrl" v-model="formData.videoUrl"

View File

@@ -252,13 +252,13 @@ const handleDeleteComponent = (index: number) => {
} }
}; };
// 注入无感刷新页面函数 // // 注入无感刷新页面函数
const reload = inject<() => void>('reload'); // TODO @芋艿:是 vue3 + element-plus 独有的,可以清理掉。 // const reload = inject<() => void>('reload'); // TODO @芋艿:是 vue3 + element-plus 独有的,可以清理掉。
// 重置 // // 重置
const handleReset = () => { // const handleReset = () => {
if (reload) reload(); // if (reload) reload();
emits('reset'); // emits('reset');
}; // };
// 预览 // 预览
const previewDialogVisible = ref(false); const previewDialogVisible = ref(false);

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PREDEFINE_COLORS } from '@vben/constants'; // import { PREDEFINE_COLORS } from '@vben/constants';
import { useVModels } from '@vueuse/core'; import { useVModels } from '@vueuse/core';
import { Input, InputGroup } from 'ant-design-vue'; import { Input, InputGroup } from 'ant-design-vue';
@@ -26,7 +26,9 @@ const { modelValue, color } = useVModels(props, emit);
<template> <template>
<InputGroup compact> <InputGroup compact>
<Input v-model:value="modelValue" v-bind="$attrs" class="flex-1" /> <Input v-model:value="modelValue" v-bind="$attrs" class="flex-1" />
<!-- TODO 芋艿后续在处理antd 不支持该组件
<ColorPicker v-model:value="color" :presets="PREDEFINE_COLORS" /> <ColorPicker v-model:value="color" :presets="PREDEFINE_COLORS" />
-->
</InputGroup> </InputGroup>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@@ -66,14 +66,10 @@ async function handleStatusChange(
}) })
.then(async () => { .then(async () => {
// 更新优惠券模板状态 // 更新优惠券模板状态
const res = await updateCouponTemplateStatus(row.id!, newStatus); await updateCouponTemplateStatus(row.id!, newStatus);
if (res) { // 提示并返回成功
// 提示并返回成功 message.success($t('ui.actionMessage.operationSuccess'));
message.success($t('ui.actionMessage.operationSuccess')); resolve(true);
resolve(true);
} else {
reject(new Error('更新失败'));
}
}) })
.catch(() => { .catch(() => {
reject(new Error('取消操作')); reject(new Error('取消操作'));

View File

@@ -67,14 +67,10 @@ async function handleStatusChange(
}) })
.then(async () => { .then(async () => {
// 更新状态 // 更新状态
const res = await updateSeckillConfigStatus(row.id, newStatus); await updateSeckillConfigStatus(row.id, newStatus);
if (res) { // 提示并返回成功
// 提示并返回成功 message.success(`${text}成功`);
message.success(`${text}成功`); resolve(true);
resolve(true);
} else {
reject(new Error($t('ui.actionMessage.operationFailed')));
}
}) })
.catch(() => { .catch(() => {
reject(new Error('取消操作')); reject(new Error('取消操作'));

View File

@@ -94,18 +94,14 @@ async function handleBrokerageEnabledChange(
}) })
.then(async () => { .then(async () => {
// 更新推广资格 // 更新推广资格
const res = await updateBrokerageEnabled({ await updateBrokerageEnabled({
id: row.id!, id: row.id!,
enabled: newEnabled, enabled: newEnabled,
}); });
if (res) { // 提示并返回成功
// 提示并返回成功 message.success($t('ui.actionMessage.operationSuccess'));
message.success($t('ui.actionMessage.operationSuccess')); handleRefresh();
handleRefresh(); resolve(true);
resolve(true);
} else {
reject(new Error('更新失败'));
}
}) })
.catch(() => { .catch(() => {
reject(new Error('取消操作')); reject(new Error('取消操作'));

View File

@@ -72,17 +72,13 @@ async function handleStatusChange(
}) })
.then(async () => { .then(async () => {
// 更新状态 // 更新状态
const res = await updateAppStatus({ await updateAppStatus({
id: row.id!, id: row.id!,
status: newStatus, status: newStatus,
}); });
if (res) { // 提示并返回成功
// 提示并返回成功 message.success(`${text}成功`);
message.success(`${text}成功`); resolve(true);
resolve(true);
} else {
reject(new Error('更新失败'));
}
}) })
.catch(() => { .catch(() => {
reject(new Error('取消操作')); reject(new Error('取消操作'));

View File

@@ -144,14 +144,10 @@ async function handleStatusChange(
}) })
.then(async () => { .then(async () => {
// 更新用户状态 // 更新用户状态
const res = await updateUserStatus(row.id!, newStatus); await updateUserStatus(row.id!, newStatus);
if (res) { // 提示并返回成功
// 提示并返回成功 message.success($t('ui.actionMessage.operationSuccess'));
message.success($t('ui.actionMessage.operationSuccess')); resolve(true);
resolve(true);
} else {
reject(new Error('更新失败'));
}
}) })
.catch(() => { .catch(() => {
reject(new Error('取消操作')); reject(new Error('取消操作'));

View File

@@ -6,6 +6,7 @@ import { requestClient } from '#/api/request';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
const accessStore = useAccessStore(); const accessStore = useAccessStore();
export namespace AiMindmapApi { export namespace AiMindmapApi {
// AI 思维导图 // AI 思维导图
export interface MindMap { export interface MindMap {
@@ -19,7 +20,7 @@ export namespace AiMindmapApi {
} }
// AI 思维导图生成 // AI 思维导图生成
export interface AiMindMapGenerateReq { export interface AiMindMapGenerateReqVO {
prompt: string; prompt: string;
} }
} }
@@ -32,7 +33,7 @@ export function generateMindMap({
ctrl, ctrl,
}: { }: {
ctrl: AbortController; ctrl: AbortController;
data: AiMindmapApi.AiMindMapGenerateReq; data: AiMindmapApi.AiMindMapGenerateReqVO;
onClose?: (...args: any[]) => void; onClose?: (...args: any[]) => void;
onError?: (...args: any[]) => void; onError?: (...args: any[]) => void;
onMessage?: (res: any) => void; onMessage?: (res: any) => void;
@@ -53,12 +54,12 @@ export function generateMindMap({
}); });
} }
// 查询思维导图分页 /** 查询思维导图分页 */
export function getMindMapPage(params: any) { export function getMindMapPage(params: any) {
return requestClient.get(`/ai/mind-map/page`, { params }); return requestClient.get(`/ai/mind-map/page`, { params });
} }
// 删除思维导图 /** 删除思维导图 */
export function deleteMindMap(id: number) { export function deleteMindMap(id: number) {
return requestClient.delete(`/ai/mind-map/delete?id=${id}`); return requestClient.delete(`/ai/mind-map/delete?id=${id}`);
} }

View File

@@ -13,7 +13,7 @@ export namespace AiModelApiKeyApi {
} }
} }
// 查询 API 密钥分页 /** 查询 API 密钥分页 */
export function getApiKeyPage(params: PageParam) { export function getApiKeyPage(params: PageParam) {
return requestClient.get<PageResult<AiModelApiKeyApi.ApiKey>>( return requestClient.get<PageResult<AiModelApiKeyApi.ApiKey>>(
'/ai/api-key/page', '/ai/api-key/page',
@@ -21,28 +21,29 @@ export function getApiKeyPage(params: PageParam) {
); );
} }
// 获得 API 密钥列表 /** 获得 API 密钥列表 */
export function getApiKeySimpleList() { export function getApiKeySimpleList() {
return requestClient.get<AiModelApiKeyApi.ApiKey[]>( return requestClient.get<AiModelApiKeyApi.ApiKey[]>(
'/ai/api-key/simple-list', '/ai/api-key/simple-list',
); );
} }
// 查询 API 密钥详情 /** 查询 API 密钥详情 */
export function getApiKey(id: number) { export function getApiKey(id: number) {
return requestClient.get<AiModelApiKeyApi.ApiKey>(`/ai/api-key/get?id=${id}`); return requestClient.get<AiModelApiKeyApi.ApiKey>(`/ai/api-key/get?id=${id}`);
} }
// 新增 API 密钥
/** 新增 API 密钥 */
export function createApiKey(data: AiModelApiKeyApi.ApiKey) { export function createApiKey(data: AiModelApiKeyApi.ApiKey) {
return requestClient.post('/ai/api-key/create', data); return requestClient.post('/ai/api-key/create', data);
} }
// 修改 API 密钥 /** 修改 API 密钥 */
export function updateApiKey(data: AiModelApiKeyApi.ApiKey) { export function updateApiKey(data: AiModelApiKeyApi.ApiKey) {
return requestClient.put('/ai/api-key/update', data); return requestClient.put('/ai/api-key/update', data);
} }
// 删除 API 密钥 /** 删除 API 密钥 */
export function deleteApiKey(id: number) { export function deleteApiKey(id: number) {
return requestClient.delete(`/ai/api-key/delete?id=${id}`); return requestClient.delete(`/ai/api-key/delete?id=${id}`);
} }

View File

@@ -20,7 +20,7 @@ export namespace AiModelChatRoleApi {
} }
// AI 聊天角色 分页请求 // AI 聊天角色 分页请求
export interface ChatRolePageReq { export interface ChatRolePageReqVO {
name?: string; // 角色名称 name?: string; // 角色名称
category?: string; // 角色类别 category?: string; // 角色类别
publicStatus: boolean; // 是否公开 publicStatus: boolean; // 是否公开
@@ -29,7 +29,7 @@ export namespace AiModelChatRoleApi {
} }
} }
// 查询聊天角色分页 /** 查询聊天角色分页 */
export function getChatRolePage(params: PageParam) { export function getChatRolePage(params: PageParam) {
return requestClient.get<PageResult<AiModelChatRoleApi.ChatRole>>( return requestClient.get<PageResult<AiModelChatRoleApi.ChatRole>>(
'/ai/chat-role/page', '/ai/chat-role/page',
@@ -37,49 +37,49 @@ export function getChatRolePage(params: PageParam) {
); );
} }
// 查询聊天角色详情 /** 查询聊天角色详情 */
export function getChatRole(id: number) { export function getChatRole(id: number) {
return requestClient.get<AiModelChatRoleApi.ChatRole>( return requestClient.get<AiModelChatRoleApi.ChatRole>(
`/ai/chat-role/get?id=${id}`, `/ai/chat-role/get?id=${id}`,
); );
} }
// 新增聊天角色
/** 新增聊天角色 */
export function createChatRole(data: AiModelChatRoleApi.ChatRole) { export function createChatRole(data: AiModelChatRoleApi.ChatRole) {
return requestClient.post('/ai/chat-role/create', data); return requestClient.post('/ai/chat-role/create', data);
} }
// 修改聊天角色 /** 修改聊天角色 */
export function updateChatRole(data: AiModelChatRoleApi.ChatRole) { export function updateChatRole(data: AiModelChatRoleApi.ChatRole) {
return requestClient.put('/ai/chat-role/update', data); return requestClient.put('/ai/chat-role/update', data);
} }
// 删除聊天角色 /** 删除聊天角色 */
export function deleteChatRole(id: number) { export function deleteChatRole(id: number) {
return requestClient.delete(`/ai/chat-role/delete?id=${id}`); return requestClient.delete(`/ai/chat-role/delete?id=${id}`);
} }
// ======= chat 聊天 /** 获取 my role */
// 获取 my role export function getMyPage(params: AiModelChatRoleApi.ChatRolePageReqVO) {
export function getMyPage(params: AiModelChatRoleApi.ChatRolePageReq) {
return requestClient.get('/ai/chat-role/my-page', { params }); return requestClient.get('/ai/chat-role/my-page', { params });
} }
// 获取角色分类 /** 获取角色分类 */
export function getCategoryList() { export function getCategoryList() {
return requestClient.get('/ai/chat-role/category-list'); return requestClient.get('/ai/chat-role/category-list');
} }
// 创建角色 /** 创建角色 */
export function createMy(data: AiModelChatRoleApi.ChatRole) { export function createMy(data: AiModelChatRoleApi.ChatRole) {
return requestClient.post('/ai/chat-role/create-my', data); return requestClient.post('/ai/chat-role/create-my', data);
} }
// 更新角色 /** 更新角色 */
export function updateMy(data: AiModelChatRoleApi.ChatRole) { export function updateMy(data: AiModelChatRoleApi.ChatRole) {
return requestClient.put('/ai/chat-role/update', data); return requestClient.put('/ai/chat-role/update', data);
} }
// 删除角色 my /** 删除角色 my */
export function deleteMy(id: number) { export function deleteMy(id: number) {
return requestClient.delete(`/ai/chat-role/delete-my?id=${id}`); return requestClient.delete(`/ai/chat-role/delete-my?id=${id}`);
} }

View File

@@ -18,7 +18,7 @@ export namespace AiModelModelApi {
} }
} }
// 查询模型分页 /** 查询模型分页 */
export function getModelPage(params: PageParam) { export function getModelPage(params: PageParam) {
return requestClient.get<PageResult<AiModelModelApi.Model>>( return requestClient.get<PageResult<AiModelModelApi.Model>>(
'/ai/model/page', '/ai/model/page',
@@ -26,7 +26,7 @@ export function getModelPage(params: PageParam) {
); );
} }
// 获得模型列表 /** 获得模型列表 */
export function getModelSimpleList(type?: number) { export function getModelSimpleList(type?: number) {
return requestClient.get<AiModelModelApi.Model[]>('/ai/model/simple-list', { return requestClient.get<AiModelModelApi.Model[]>('/ai/model/simple-list', {
params: { params: {
@@ -35,21 +35,22 @@ export function getModelSimpleList(type?: number) {
}); });
} }
// 查询模型详情 /** 查询模型详情 */
export function getModel(id: number) { export function getModel(id: number) {
return requestClient.get<AiModelModelApi.Model>(`/ai/model/get?id=${id}`); return requestClient.get<AiModelModelApi.Model>(`/ai/model/get?id=${id}`);
} }
// 新增模型
/** 新增模型 */
export function createModel(data: AiModelModelApi.Model) { export function createModel(data: AiModelModelApi.Model) {
return requestClient.post('/ai/model/create', data); return requestClient.post('/ai/model/create', data);
} }
// 修改模型 /** 修改模型 */
export function updateModel(data: AiModelModelApi.Model) { export function updateModel(data: AiModelModelApi.Model) {
return requestClient.put('/ai/model/update', data); return requestClient.put('/ai/model/update', data);
} }
// 删除模型 /** 删除模型 */
export function deleteModel(id: number) { export function deleteModel(id: number) {
return requestClient.delete(`/ai/model/delete?id=${id}`); return requestClient.delete(`/ai/model/delete?id=${id}`);
} }

View File

@@ -11,33 +11,34 @@ export namespace AiModelToolApi {
} }
} }
// 查询工具分页 /** 查询工具分页 */
export function getToolPage(params: PageParam) { export function getToolPage(params: PageParam) {
return requestClient.get<PageResult<AiModelToolApi.Tool>>('/ai/tool/page', { return requestClient.get<PageResult<AiModelToolApi.Tool>>('/ai/tool/page', {
params, params,
}); });
} }
// 查询工具详情 /** 查询工具详情 */
export function getTool(id: number) { export function getTool(id: number) {
return requestClient.get<AiModelToolApi.Tool>(`/ai/tool/get?id=${id}`); return requestClient.get<AiModelToolApi.Tool>(`/ai/tool/get?id=${id}`);
} }
// 新增工具
/** 新增工具 */
export function createTool(data: AiModelToolApi.Tool) { export function createTool(data: AiModelToolApi.Tool) {
return requestClient.post('/ai/tool/create', data); return requestClient.post('/ai/tool/create', data);
} }
// 修改工具 /** 修改工具 */
export function updateTool(data: AiModelToolApi.Tool) { export function updateTool(data: AiModelToolApi.Tool) {
return requestClient.put('/ai/tool/update', data); return requestClient.put('/ai/tool/update', data);
} }
// 删除工具 /** 删除工具 */
export function deleteTool(id: number) { export function deleteTool(id: number) {
return requestClient.delete(`/ai/tool/delete?id=${id}`); return requestClient.delete(`/ai/tool/delete?id=${id}`);
} }
// 获取工具简单列表 /** 获取工具简单列表 */
export function getToolSimpleList() { export function getToolSimpleList() {
return requestClient.get<AiModelToolApi.Tool[]>('/ai/tool/simple-list'); return requestClient.get<AiModelToolApi.Tool[]>('/ai/tool/simple-list');
} }

View File

@@ -9,8 +9,10 @@ import { requestClient } from '#/api/request';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
const accessStore = useAccessStore(); const accessStore = useAccessStore();
export namespace AiWriteApi { export namespace AiWriteApi {
export interface Write { export interface Write {
id?: number;
type: AiWriteTypeEnum.REPLY | AiWriteTypeEnum.WRITING; // 1:撰写 2:回复 type: AiWriteTypeEnum.REPLY | AiWriteTypeEnum.WRITING; // 1:撰写 2:回复
prompt: string; // 写作内容提示 1。撰写 2回复 prompt: string; // 写作内容提示 1。撰写 2回复
originalContent: string; // 原文 originalContent: string; // 原文
@@ -26,29 +28,12 @@ export namespace AiWriteApi {
createTime?: Date; // 创建时间 createTime?: Date; // 创建时间
} }
export interface AiWritePageReq extends PageParam { export interface AiWritePageReqVO extends PageParam {
userId?: number; // 用户编号 userId?: number; // 用户编号
type?: AiWriteTypeEnum; // 写作类型 type?: AiWriteTypeEnum; // 写作类型
platform?: string; // 平台 platform?: string; // 平台
createTime?: [string, string]; // 创建时间 createTime?: [string, string]; // 创建时间
} }
export interface AiWriteResp {
id: number;
userId: number;
type: number;
platform: string;
model: string;
prompt: string;
generatedContent: string;
originalContent: string;
length: number;
format: number;
tone: number;
language: number;
errorMessage: string;
createTime: string;
}
} }
export function writeStream({ export function writeStream({
@@ -80,15 +65,14 @@ export function writeStream({
}); });
} }
// 获取写作列表 /** 获取写作列表 */
export function getWritePage(params: any) { export function getWritePage(params: AiWriteApi.AiWritePageReqVO) {
return requestClient.get<PageResult<AiWriteApi.AiWritePageReq>>( return requestClient.get<PageResult<AiWriteApi.Write>>(`/ai/write/page`, {
`/ai/write/page`, params,
{ params }, });
);
} }
// 删除音乐 /** 删除写作记录 */
export function deleteWrite(id: number) { export function deleteWrite(id: number) {
return requestClient.delete(`/ai/write/delete`, { params: { id } }); return requestClient.delete(`/ai/write/delete`, { params: { id } });
} }

View File

@@ -10,6 +10,7 @@ import { get, getNestedValue, isFunction } from '@vben/utils';
import { ElDescriptions, ElDescriptionsItem } from 'element-plus'; import { ElDescriptions, ElDescriptionsItem } from 'element-plus';
const props = { const props = {
// TODO @星语bordered 不生效;之前好像是 border
bordered: { default: true, type: Boolean }, bordered: { default: true, type: Boolean },
column: { column: {
default: () => { default: () => {
@@ -127,6 +128,7 @@ export default defineComponent({
}, },
default: () => { default: () => {
if (item.slot) { if (item.slot) {
// TODO @xingyu这里要 inline 掉么?
const slotContent = getSlot(slots, item.slot, data); const slotContent = getSlot(slots, item.slot, data);
return slotContent; return slotContent;
} }

View File

@@ -1,2 +0,0 @@
export { default as SummaryCard } from './summary-card.vue';
export type { SummaryCardProps } from './typing';

View File

@@ -1,57 +0,0 @@
<script lang="ts" setup>
import type { SummaryCardProps } from './typing';
import { CountTo } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { ElTooltip } from 'element-plus';
/** 统计卡片 */
defineOptions({ name: 'SummaryCard' });
defineProps<SummaryCardProps>();
</script>
<template>
<div
class="flex flex-row items-center gap-3 rounded bg-[var(--el-bg-color-overlay)] p-4"
>
<div
class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded"
:class="`${iconColor} ${iconBgColor}`"
>
<IconifyIcon v-if="icon" :icon="icon" class="!text-6" />
</div>
<div class="flex flex-col gap-1">
<div class="flex items-center gap-1">
<span class="text-base">{{ title }}</span>
<ElTooltip :content="tooltip" placement="topLeft" v-if="tooltip">
<IconifyIcon
icon="lucide:circle-alert"
class="item-center !text-3 flex"
/>
</ElTooltip>
</div>
<div class="flex flex-row items-baseline gap-2">
<div class="text-lg">
<CountTo
:prefix="prefix"
:end-val="value ?? 0"
:decimals="decimals ?? 0"
/>
</div>
<span
v-if="percent !== undefined"
:class="Number(percent) > 0 ? 'text-red-500' : 'text-green-500'"
>
<span class="text-sm">{{ Math.abs(Number(percent)) }}%</span>
<IconifyIcon
:icon="
Number(percent) > 0 ? 'lucide:chevron-up' : 'lucide:chevron-down'
"
class="ml-0.5 !text-sm"
/>
</span>
</div>
</div>
</div>
</template>

View File

@@ -1,11 +0,0 @@
export interface SummaryCardProps {
title: string;
tooltip?: string;
icon?: string;
iconColor?: string;
iconBgColor?: string;
prefix?: string;
value?: number;
decimals?: number;
percent?: number | string;
}

View File

@@ -0,0 +1,223 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/api/system/user';
import { DICT_TYPE } from '@vben/constants';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
/** 关联数据 */
let userList: SystemUserApi.User[] = [];
getSimpleUserList().then((data) => (userList = data));
/** 列表的搜索表单 */
export function useGridFormSchemaConversation(): VbenFormSchema[] {
return [
{
fieldName: 'userId',
label: '用户编号',
component: 'Input',
componentProps: {
placeholder: '请输入用户编号',
clearable: true,
},
},
{
fieldName: 'title',
label: '聊天标题',
component: 'Input',
componentProps: {
placeholder: '请输入聊天标题',
clearable: true,
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumnsConversation(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '对话编号',
fixed: 'left',
minWidth: 180,
},
{
field: 'title',
title: '对话标题',
minWidth: 180,
fixed: 'left',
},
{
title: '用户',
minWidth: 180,
field: 'userId',
formatter: ({ cellValue }) => {
if (cellValue === 0) {
return '系统';
}
return userList.find((user) => user.id === cellValue)?.nickname || '-';
},
},
{
field: 'roleName',
title: '角色',
minWidth: 180,
},
{
field: 'model',
title: '模型标识',
minWidth: 180,
},
{
field: 'messageCount',
title: '消息数',
minWidth: 180,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'temperature',
title: '温度参数',
minWidth: 80,
},
{
title: '回复数 Token 数',
field: 'maxTokens',
minWidth: 120,
},
{
title: '上下文数量',
field: 'maxContexts',
minWidth: 120,
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchemaMessage(): VbenFormSchema[] {
return [
{
fieldName: 'conversationId',
label: '对话编号',
component: 'Input',
componentProps: {
placeholder: '请输入对话编号',
clearable: true,
},
},
{
fieldName: 'userId',
label: '用户编号',
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
labelField: 'nickname',
valueField: 'id',
placeholder: '请选择用户编号',
clearable: true,
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumnsMessage(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '消息编号',
fixed: 'left',
minWidth: 180,
},
{
field: 'conversationId',
title: '对话编号',
minWidth: 180,
fixed: 'left',
},
{
title: '用户',
minWidth: 180,
field: 'userId',
formatter: ({ cellValue }) =>
userList.find((user) => user.id === cellValue)?.nickname || '-',
},
{
field: 'roleName',
title: '角色',
minWidth: 180,
},
{
field: 'type',
title: '消息类型',
minWidth: 100,
},
{
field: 'model',
title: '模型标识',
minWidth: 180,
},
{
field: 'content',
title: '消息内容',
minWidth: 300,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'replyId',
title: '回复消息编号',
minWidth: 180,
},
{
title: '携带上下文',
field: 'useContext',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
minWidth: 100,
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -0,0 +1,29 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { DocAlert, Page } from '@vben/common-ui';
import { ElTabs } from 'element-plus';
import ChatConversationList from './modules/conversation-list.vue';
import ChatMessageList from './modules/message-list.vue';
const activeTabName = ref('conversation');
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
</template>
<ElTabs v-model:model-value="activeTabName">
<ElTabs.TabPane label="对话列表" name="conversation">
<ChatConversationList />
</ElTabs.TabPane>
<ElTabs.TabPane label="消息列表" name="message">
<ChatMessageList />
</ElTabs.TabPane>
</ElTabs>
</Page>
</template>

View File

@@ -0,0 +1,93 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiChatConversationApi } from '#/api/ai/chat/conversation';
import { Page } from '@vben/common-ui';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteChatConversationByAdmin,
getChatConversationPage,
} from '#/api/ai/chat/conversation';
import { $t } from '#/locales';
import {
useGridColumnsConversation,
useGridFormSchemaConversation,
} from '../data';
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 删除对话 */
async function handleDelete(row: AiChatConversationApi.ChatConversation) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.id]),
});
try {
await deleteChatConversationByAdmin(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchemaConversation(),
},
gridOptions: {
columns: useGridColumnsConversation(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getChatConversationPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<AiChatConversationApi.ChatConversation>,
});
</script>
<template>
<Page auto-content-height>
<Grid table-title="对话列表">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['ai:chat-conversation:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiChatConversationApi } from '#/api/ai/chat/conversation';
import { Page } from '@vben/common-ui';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteChatMessageByAdmin,
getChatMessagePage,
} from '#/api/ai/chat/message';
import { $t } from '#/locales';
import { useGridColumnsMessage, useGridFormSchemaMessage } from '../data';
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 删除消息 */
async function handleDelete(row: AiChatConversationApi.ChatConversation) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.id]),
});
try {
await deleteChatMessageByAdmin(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchemaMessage(),
},
gridOptions: {
columns: useGridColumnsMessage(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getChatMessagePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<AiChatConversationApi.ChatConversation>,
});
</script>
<template>
<Page auto-content-height>
<Grid table-title="消息列表">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['ai:chat-message:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,180 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/api/system/user';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
let userList: SystemUserApi.User[] = [];
async function getUserData() {
userList = await getSimpleUserList();
}
getUserData();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'userId',
label: '用户编号',
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
labelField: 'nickname',
valueField: 'id',
placeholder: '请选择用户编号',
clearable: true,
},
},
{
fieldName: 'platform',
label: '平台',
component: 'Select',
componentProps: {
placeholder: '请选择平台',
clearable: true,
options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
},
},
{
fieldName: 'status',
label: '绘画状态',
component: 'Select',
componentProps: {
placeholder: '请选择绘画状态',
clearable: true,
options: getDictOptions(DICT_TYPE.AI_IMAGE_STATUS, 'number'),
},
},
{
fieldName: 'publicStatus',
label: '是否发布',
component: 'Select',
componentProps: {
placeholder: '请选择是否发布',
clearable: true,
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(
onPublicStatusChange?: (
newStatus: boolean,
row: any,
) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '编号',
minWidth: 180,
fixed: 'left',
},
{
field: 'picUrl',
title: '图片',
minWidth: 110,
fixed: 'left',
cellRender: {
name: 'CellImage',
},
},
{
field: 'userId',
title: '用户',
minWidth: 180,
formatter: ({ cellValue }) =>
userList.find((user) => user.id === cellValue)?.nickname || '-',
},
{
field: 'platform',
title: '平台',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.AI_PLATFORM },
},
},
{
field: 'model',
title: '模型',
minWidth: 180,
},
{
field: 'status',
title: '绘画状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.AI_IMAGE_STATUS },
},
},
{
minWidth: 100,
title: '是否发布',
field: 'publicStatus',
align: 'center',
cellRender: {
attrs: { beforeChange: onPublicStatusChange },
name: 'CellSwitch',
props: {
activeValue: true,
inactiveValue: false,
},
},
},
{
field: 'prompt',
title: '提示词',
minWidth: 180,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'width',
title: '宽度',
minWidth: 180,
},
{
field: 'height',
title: '高度',
minWidth: 180,
},
{
field: 'errorMessage',
title: '错误信息',
minWidth: 180,
},
{
field: 'taskId',
title: '任务编号',
minWidth: 180,
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -0,0 +1,116 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiImageApi } from '#/api/ai/image';
import { confirm, DocAlert, Page } from '@vben/common-ui';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteImage, getImagePage, updateImage } from '#/api/ai/image';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 删除图片 */
async function handleDelete(row: AiImageApi.Image) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.id]),
});
try {
await deleteImage(row.id as number);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 修改是否发布 */
async function handleUpdatePublicStatusChange(
newStatus: boolean,
row: AiImageApi.Image,
): Promise<boolean | undefined> {
const text = newStatus ? '公开' : '私有';
return new Promise((resolve, reject) => {
confirm({
content: `确认要将该图片切换为【${text}】吗?`,
})
.then(async () => {
// 更新图片状态
await updateImage({
id: row.id,
publicStatus: newStatus,
});
// 提示并返回成功
ElMessage.success($t('ui.actionMessage.operationSuccess'));
resolve(true);
})
.catch(() => {
reject(new Error('取消操作'));
});
});
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(handleUpdatePublicStatusChange),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getImagePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<AiImageApi.Image>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert title="AI 绘图创作" url="https://doc.iocoder.cn/ai/image/" />
</template>
<Grid table-title="绘画管理列表">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['ai:image:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,99 @@
<script lang="ts" setup>
import type { AiMindmapApi } from '#/api/ai/mindmap';
import { nextTick, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { MindMapContentExample } from '@vben/constants';
import { ElMessage } from 'element-plus';
import { generateMindMap } from '#/api/ai/mindmap';
import Left from './modules/left.vue';
import Right from './modules/right.vue';
const ctrl = ref<AbortController>(); // 请求控制
const isGenerating = ref(false); // 是否正在生成思维导图
const isStart = ref(false); // 开始生成,用来清空思维导图
const isEnd = ref(true); // 用来判断结束的时候渲染思维导图
const generatedContent = ref(''); // 生成思维导图结果
const leftRef = ref<InstanceType<typeof Left>>(); // 左边组件
const rightRef = ref(); // 右边组件
/** 使用已有内容直接生成 */
function directGenerate(existPrompt: string) {
isEnd.value = false; // 先设置为 false 再设置为 true让子组建的 watch 能够监听到
generatedContent.value = existPrompt;
isEnd.value = true;
}
/** 提交生成 */
function handleSubmit(data: AiMindmapApi.AiMindMapGenerateReqVO) {
isGenerating.value = true;
isStart.value = true;
isEnd.value = false;
ctrl.value = new AbortController(); // 请求控制赋值
generatedContent.value = ''; // 清空生成数据
generateMindMap({
data,
onMessage: async (res: any) => {
const { code, data, msg } = JSON.parse(res.data);
if (code !== 0) {
ElMessage.error(`生成思维导图异常! ${msg}`);
handleStopStream();
return;
}
generatedContent.value = generatedContent.value + data;
await nextTick();
rightRef.value?.scrollBottom();
},
onClose() {
isEnd.value = true;
leftRef.value?.setGeneratedContent(generatedContent.value);
handleStopStream();
},
onError(err) {
console.error('生成思维导图失败', err);
handleStopStream();
// 需要抛出异常,禁止重试
throw err;
},
ctrl: ctrl.value,
});
}
/** 停止 stream 生成 */
function handleStopStream() {
isGenerating.value = false;
isStart.value = false;
ctrl.value?.abort();
}
/** 初始化 */
onMounted(() => {
generatedContent.value = MindMapContentExample;
});
</script>
<template>
<Page auto-content-height>
<div class="absolute bottom-0 left-0 right-0 top-0 m-4 flex">
<Left
ref="leftRef"
class="mr-4"
:is-generating="isGenerating"
@submit="handleSubmit"
@direct-generate="directGenerate"
/>
<Right
ref="rightRef"
:generated-content="generatedContent"
:is-end="isEnd"
:is-generating="isGenerating"
:is-start="isStart"
/>
</div>
</Page>
</template>

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { MindMapContentExample } from '@vben/constants';
import { ElButton, ElInput } from 'element-plus';
defineProps<{
isGenerating: boolean;
}>();
const emits = defineEmits(['submit', 'directGenerate']);
const formData = reactive({
prompt: '',
});
const generatedContent = ref(MindMapContentExample); // 已有的内容
defineExpose({
setGeneratedContent(newContent: string) {
// 设置已有的内容,在生成结束的时候将结果赋值给该值
generatedContent.value = newContent;
},
});
</script>
<template>
<div class="bg-card flex w-80 flex-col rounded-lg p-5">
<h3 class="text-primary h-7 w-full text-center text-xl leading-7">
思维导图创作中心
</h3>
<div class="mt-4 flex-grow overflow-y-auto">
<div>
<b>您的需求</b>
<ElInput
v-model="formData.prompt"
type="textarea"
:maxlength="1024"
:rows="8"
class="mt-4 w-full"
placeholder="请输入提示词让AI帮你完善"
show-word-limit
/>
<ElButton
class="mt-4 !w-full"
type="primary"
:loading="isGenerating"
@click="emits('submit', formData)"
>
智能生成思维导图
</ElButton>
</div>
<div class="mt-7">
<b>使用已有内容生成</b>
<ElInput
v-model="generatedContent"
type="textarea"
:maxlength="1024"
:rows="8"
class="mt-4 w-full"
placeholder="例如:童话里的小屋应该是什么样子?"
show-word-limit
/>
<ElButton
class="mt-4 !w-full"
type="primary"
@click="emits('directGenerate', generatedContent)"
:disabled="isGenerating"
>
直接生成
</ElButton>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,215 @@
<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import {
MarkdownIt,
Markmap,
Toolbar,
Transformer,
} from '@vben/plugins/markmap';
import { downloadImageByCanvas } from '@vben/utils';
import { ElButton, ElCard, ElMessage } from 'element-plus';
const props = defineProps<{
generatedContent: string; // 生成结果
isEnd: boolean; // 是否结束
isGenerating: boolean; // 是否正在生成
isStart: boolean; // 开始状态,开始时需要清除 html
}>();
const md = MarkdownIt();
const contentRef = ref<HTMLDivElement>(); // 右侧出来 header 以下的区域
const mdContainerRef = ref<HTMLDivElement>(); // markdown 的容器,用来滚动到底下的
const mindMapRef = ref<HTMLDivElement>(); // 思维导图的容器
const svgRef = ref<SVGElement>(); // 思维导图的渲染 svg
const toolBarRef = ref<HTMLDivElement>(); // 思维导图右下角的工具栏,缩放等
const html = ref(''); // 生成过程中的文本
const contentAreaHeight = ref(0); // 生成区域的高度,出去 header 部分
let markMap: Markmap | null = null;
const transformer = new Transformer();
let resizeObserver: null | ResizeObserver = null;
const initialized = false;
/** 初始化 */
onMounted(() => {
resizeObserver = new ResizeObserver(() => {
contentAreaHeight.value = contentRef.value?.clientHeight || 0;
// 先更新高度,再更新思维导图
if (contentAreaHeight.value && !initialized) {
// 初始化思维导图
try {
if (!markMap) {
markMap = Markmap.create(svgRef.value!);
const { el } = Toolbar.create(markMap);
toolBarRef.value?.append(el);
}
nextTick(update);
} catch {
ElMessage.error('思维导图初始化失败');
}
}
});
if (contentRef.value) {
resizeObserver.observe(contentRef.value);
}
});
/** 卸载 */
onBeforeUnmount(() => {
if (resizeObserver && contentRef.value) {
resizeObserver.unobserve(contentRef.value);
}
});
/** 监听 props 变化 */
watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => {
// 开始生成的时候清空一下 markdown 的内容
if (isStart) {
html.value = '';
}
// 生成内容的时候使用 markdown 来渲染
if (isGenerating) {
html.value = md.render(generatedContent);
}
// 生成结束时更新思维导图
if (isEnd) {
update();
}
});
/** 更新思维导图的展示 */
function update() {
try {
const { root } = transformer.transform(
processContent(props.generatedContent),
);
markMap?.setData(root);
markMap?.fit();
} catch (error: any) {
console.error(error);
}
}
/** 处理内容 */
function processContent(text: string) {
const arr: string[] = [];
const lines = text.split('\n');
for (let line of lines) {
if (line.includes('```')) {
continue;
}
// eslint-disable-next-line unicorn/prefer-string-replace-all
line = line.replace(/([*_~`>])|(\d+\.)\s/g, '');
arr.push(line);
}
return arr.join('\n');
}
/** 下载图片download SVG to png file */
function downloadImage() {
const svgElement = mindMapRef.value;
// 将 SVG 渲染到图片对象
const serializer = new XMLSerializer();
const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgRef.value!)}`;
const base64Url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`;
downloadImageByCanvas({
url: base64Url,
canvasWidth: svgElement?.offsetWidth,
canvasHeight: svgElement?.offsetHeight,
drawWithImageSize: false,
});
}
defineExpose({
scrollBottom() {
mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight);
},
});
</script>
<template>
<ElCard class="my-card flex h-full flex-grow flex-col">
<template #header>
<div class="m-0 flex shrink-0 items-center justify-between px-7">
<h3>思维导图预览</h3>
<ElButton
type="primary"
size="small"
class="flex"
@click="downloadImage"
>
<template #icon>
<div class="flex items-center justify-center">
<IconifyIcon icon="lucide:copy" />
</div>
</template>
下载图片
</ElButton>
</div>
</template>
<div ref="contentRef" class="hide-scroll-bar box-border h-full">
<div
v-if="isGenerating"
ref="mdContainerRef"
class="wh-full overflow-y-auto"
>
<div
class="flex flex-col items-center justify-center"
v-html="html"
></div>
</div>
<div ref="mindMapRef" class="wh-full">
<svg
ref="svgRef"
:style="{ height: `${contentAreaHeight}px` }"
class="w-full"
/>
<div ref="toolBarRef" class="absolute bottom-2.5 right-5"></div>
</div>
</div>
</ElCard>
</template>
<style lang="scss" scoped>
// 定义一个 mixin 替代 extend
@mixin hide-scroll-bar {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
.hide-scroll-bar {
@include hide-scroll-bar;
}
.my-card {
:deep(.el-card__body) {
box-sizing: border-box;
flex-grow: 1;
padding: 0;
overflow-y: auto;
@include hide-scroll-bar;
}
}
// markmap的tool样式覆盖
:deep(.markmap) {
width: 100%;
}
:deep(.mm-toolbar-brand) {
display: none;
}
:deep(.mm-toolbar) {
display: flex;
flex-direction: row;
}
</style>

View File

@@ -0,0 +1,97 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/api/system/user';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
/** 关联数据 */
let userList: SystemUserApi.User[] = [];
getSimpleUserList().then((data) => (userList = data));
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'userId',
label: '用户编号',
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
labelField: 'nickname',
valueField: 'id',
placeholder: '请选择用户',
allowClear: true,
},
},
{
fieldName: 'prompt',
label: '提示词',
component: 'Input',
componentProps: {
placeholder: '请输入提示词',
clearable: true,
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '编号',
minWidth: 180,
fixed: 'left',
},
{
field: 'userId',
title: '用户',
minWidth: 180,
formatter: ({ cellValue }) =>
userList.find((user) => user.id === cellValue)?.nickname || '-',
},
{
field: 'prompt',
title: '提示词',
minWidth: 180,
},
{
field: 'generatedContent',
title: '思维导图',
minWidth: 300,
},
{
field: 'model',
title: '模型',
minWidth: 180,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'errorMessage',
title: '错误信息',
minWidth: 180,
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -0,0 +1,120 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiMindmapApi } from '#/api/ai/mindmap';
import { DocAlert, Page, useVbenDrawer } from '@vben/common-ui';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteMindMap, getMindMapPage } from '#/api/ai/mindmap';
import { $t } from '#/locales';
import Right from '../index/modules/right.vue';
import { useGridColumns, useGridFormSchema } from './data';
const [Drawer, drawerApi] = useVbenDrawer({
header: false,
footer: false,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 删除思维导图记录 */
async function handleDelete(row: AiMindmapApi.MindMap) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.id]),
});
try {
await deleteMindMap(row.id as number);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 预览思维导图 */
async function openPreview(row: AiMindmapApi.MindMap) {
drawerApi.setData(row.generatedContent).open();
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getMindMapPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<AiMindmapApi.MindMap>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert title="AI 思维导图" url="https://doc.iocoder.cn/ai/mindmap/" />
</template>
<Drawer class="w-3/5">
<Right
:generated-content="drawerApi.getData() as any"
:is-end="true"
:is-generating="false"
:is-start="false"
/>
</Drawer>
<Grid table-title="思维导图管理列表">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('ui.cropper.preview'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['ai:api-key:update'],
disabled: !row.generatedContent,
onClick: openPreview.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['ai:mind-map:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,147 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'platform',
label: '所属平台',
component: 'Select',
componentProps: {
placeholder: '请选择所属平台',
options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
allowClear: true,
},
rules: 'required',
},
{
component: 'Input',
fieldName: 'name',
label: '名称',
rules: 'required',
componentProps: {
placeholder: '请输入名称',
},
},
{
component: 'Input',
fieldName: 'apiKey',
label: '密钥',
rules: 'required',
componentProps: {
placeholder: '请输入密钥',
},
},
{
component: 'Input',
fieldName: 'url',
label: '自定义 API URL',
componentProps: {
placeholder: '请输入自定义 API URL',
},
},
{
fieldName: 'status',
label: '状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '名称',
component: 'Input',
componentProps: {
placeholder: '请输入名称',
allowClear: true,
},
},
{
fieldName: 'platform',
label: '平台',
component: 'Select',
componentProps: {
allowClear: true,
placeholder: '请选择平台',
options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
placeholder: '请选择状态',
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'platform',
title: '所属平台',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.AI_PLATFORM },
},
minWidth: 100,
},
{
field: 'name',
title: '名称',
minWidth: 120,
},
{
field: 'apiKey',
title: '密钥',
minWidth: 140,
},
{
field: 'url',
title: '自定义 API URL',
minWidth: 180,
},
{
field: 'status',
title: '状态',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
minWidth: 80,
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -0,0 +1,128 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiModelApiKeyApi } from '#/api/ai/model/apiKey';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteApiKey, getApiKeyPage } from '#/api/ai/model/apiKey';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建 API 密钥 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑 API 密钥 */
function handleEdit(row: AiModelApiKeyApi.ApiKey) {
formModalApi.setData(row).open();
}
/** 删除 API 密钥 */
async function handleDelete(row: AiModelApiKeyApi.ApiKey) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.name]),
});
try {
await deleteApiKey(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getApiKeyPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<AiModelApiKeyApi.ApiKey>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert title="AI 手册" url="https://doc.iocoder.cn/ai/build/" />
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="API 密钥列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['API 密钥']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['ai:api-key:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['ai:api-key:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['ai:api-key:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { AiModelApiKeyApi } from '#/api/ai/model/apiKey';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { createApiKey, getApiKey, updateApiKey } from '#/api/ai/model/apiKey';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<AiModelApiKeyApi.ApiKey>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['API 密钥'])
: $t('ui.actionTitle.create', ['API 密钥']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 120,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as AiModelApiKeyApi.ApiKey;
try {
await (formData.value?.id ? updateApiKey(data) : createApiKey(data));
// 关闭并提示
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<AiModelApiKeyApi.ApiKey>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getApiKey(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,312 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { AiModelTypeEnum, CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
import { getSimpleKnowledgeList } from '#/api/ai/knowledge/knowledge';
import { getModelSimpleList } from '#/api/ai/model/model';
import { getToolSimpleList } from '#/api/ai/model/tool';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
component: 'Input',
fieldName: 'formType',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
component: 'Input',
fieldName: 'name',
label: '角色名称',
rules: 'required',
componentProps: {
placeholder: '请输入角色名称',
},
},
{
component: 'ImageUpload',
fieldName: 'avatar',
label: '角色头像',
rules: 'required',
},
{
fieldName: 'modelId',
label: '绑定模型',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择绑定模型',
api: () => getModelSimpleList(AiModelTypeEnum.CHAT),
labelField: 'name',
valueField: 'id',
allowClear: true,
},
dependencies: {
triggerFields: ['formType'],
show: (values) => {
return values.formType === 'create' || values.formType === 'update';
},
},
},
{
component: 'Input',
fieldName: 'category',
label: '角色类别',
rules: 'required',
componentProps: {
placeholder: '请输入角色类别',
},
dependencies: {
triggerFields: ['formType'],
show: (values) => {
return values.formType === 'create' || values.formType === 'update';
},
},
},
{
component: 'Textarea',
fieldName: 'description',
label: '角色描述',
componentProps: {
placeholder: '请输入角色描述',
},
rules: 'required',
},
{
fieldName: 'systemMessage',
label: '角色设定',
component: 'Textarea',
componentProps: {
placeholder: '请输入角色设定',
},
rules: 'required',
},
{
fieldName: 'knowledgeIds',
label: '引用知识库',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择引用知识库',
api: getSimpleKnowledgeList,
labelField: 'name',
mode: 'multiple',
valueField: 'id',
allowClear: true,
},
},
{
fieldName: 'toolIds',
label: '引用工具',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择引用工具',
api: getToolSimpleList,
mode: 'multiple',
labelField: 'name',
valueField: 'id',
allowClear: true,
},
},
{
fieldName: 'mcpClientNames',
label: '引用 MCP',
component: 'Select',
componentProps: {
placeholder: '请选择 MCP',
options: getDictOptions(DICT_TYPE.AI_MCP_CLIENT_NAME, 'string'),
mode: 'multiple',
allowClear: true,
},
},
{
fieldName: 'publicStatus',
label: '是否公开',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
},
defaultValue: true,
dependencies: {
triggerFields: ['formType'],
show: (values) => {
return values.formType === 'create' || values.formType === 'update';
},
},
rules: 'required',
},
{
fieldName: 'sort',
label: '角色排序',
component: 'InputNumber',
componentProps: {
placeholder: '请输入角色排序',
controlsPosition: 'right',
class: '!w-full',
},
dependencies: {
triggerFields: ['formType'],
show: (values) => {
return values.formType === 'create' || values.formType === 'update';
},
},
rules: 'required',
},
{
fieldName: 'status',
label: '开启状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
dependencies: {
triggerFields: ['formType'],
show: (values) => {
return values.formType === 'create' || values.formType === 'update';
},
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '角色名称',
component: 'Input',
},
{
fieldName: 'category',
label: '角色类别',
component: 'Input',
},
{
fieldName: 'publicStatus',
label: '是否公开',
component: 'Select',
componentProps: {
placeholder: '请选择是否公开',
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
allowClear: true,
},
defaultValue: true,
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'name',
title: '角色名称',
minWidth: 100,
},
{
title: '绑定模型',
field: 'modelName',
minWidth: 100,
},
{
title: '角色头像',
field: 'avatar',
minWidth: 140,
cellRender: {
name: 'CellImage',
props: {
width: 40,
height: 40,
},
},
},
{
title: '角色类别',
field: 'category',
minWidth: 100,
},
{
title: '角色描述',
field: 'description',
minWidth: 100,
},
{
title: '角色设定',
field: 'systemMessage',
minWidth: 100,
},
{
title: '知识库',
field: 'knowledgeIds',
minWidth: 100,
formatter: ({ cellValue }) => {
return !cellValue || cellValue.length === 0
? '-'
: `引用${cellValue.length}`;
},
},
{
title: '工具',
field: 'toolIds',
minWidth: 100,
formatter: ({ cellValue }) => {
return !cellValue || cellValue.length === 0
? '-'
: `引用${cellValue.length}`;
},
},
{
title: 'MCP',
field: 'mcpClientNames',
minWidth: 100,
formatter: ({ cellValue }) => {
return !cellValue || cellValue.length === 0
? '-'
: `引用${cellValue.length}`;
},
},
{
field: 'publicStatus',
title: '是否公开',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
minWidth: 80,
},
{
field: 'status',
title: '状态',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
minWidth: 80,
},
{
title: '角色排序',
field: 'sort',
minWidth: 80,
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -0,0 +1,128 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiModelChatRoleApi } from '#/api/ai/model/chatRole';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteChatRole, getChatRolePage } from '#/api/ai/model/chatRole';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建聊天角色 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 编辑聊天角色 */
function handleEdit(row: AiModelChatRoleApi.ChatRole) {
formModalApi.setData({ formType: 'update', ...row }).open();
}
/** 删除聊天角色 */
async function handleDelete(row: AiModelChatRoleApi.ChatRole) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.name]),
});
try {
await deleteChatRole(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getChatRolePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<AiModelChatRoleApi.ChatRole>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="聊天角色列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['聊天角色']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['ai:chat-role:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['ai:chat-role:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['ai:chat-role:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

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