Merge remote-tracking branch 'yudao/dev' into dev
This commit is contained in:
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 } });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,7 +16,7 @@ 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 />
|
||||||
@@ -25,6 +25,5 @@ const activeTabName = ref('conversation');
|
|||||||
<ChatMessageList />
|
<ChatMessageList />
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Card>
|
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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="[
|
||||||
@@ -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="[
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 ? '公开' : '私有';
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
confirm({
|
||||||
|
content: `确认要将该图片切换为【${text}】吗?`,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
// 更新图片状态
|
||||||
await updateImage({
|
await updateImage({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
publicStatus: row.publicStatus,
|
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="[
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: '',
|
||||||
});
|
});
|
||||||
@@ -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"
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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: '操作',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: '是否公开',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -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: '操作',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 ? '公开' : '私有';
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
confirm({
|
||||||
|
content: `确认要将该音乐切换为【${text}】吗?`,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
// 更新音乐状态
|
||||||
await updateMusic({
|
await updateMusic({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
publicStatus: row.publicStatus,
|
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="[
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>; // 用来记录切换之前所填写的数据,切换的时候给赋值回来
|
||||||
|
|
||||||
/** 用来记录切换之前所填写的数据,切换的时候给赋值回来 */
|
|
||||||
const recordFormData = {} as Record<AiWriteTypeEnum, AiWriteApi.Write>;
|
|
||||||
/** 切换 tab */
|
/** 切换 tab */
|
||||||
function switchTab(value: TabType) {
|
function handleSwitchTab(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>
|
||||||
@@ -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="生成的内容……"
|
||||||
/>
|
/>
|
||||||
@@ -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;
|
||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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('转化客户成功');
|
message.success('转化客户成功');
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} else {
|
|
||||||
reject(new Error('转化失败'));
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
reject(new Error('取消操作'));
|
reject(new Error('取消操作'));
|
||||||
|
|||||||
@@ -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 ? '锁定客户成功' : '解锁客户成功');
|
message.success(lockStatus ? '锁定客户成功' : '解锁客户成功');
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} else {
|
|
||||||
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('领取客户成功');
|
message.success('领取客户成功');
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} else {
|
|
||||||
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('放入公海成功');
|
message.success('放入公海成功');
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} else {
|
|
||||||
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,
|
// 提示并返回成功
|
||||||
);
|
|
||||||
if (res) {
|
|
||||||
message.success('更新成交状态成功');
|
message.success('更新成交状态成功');
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} else {
|
|
||||||
reject(new Error('更新成交状态失败'));
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
reject(new Error('取消操作'));
|
reject(new Error('取消操作'));
|
||||||
|
|||||||
@@ -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);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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('取消操作'));
|
||||||
|
|||||||
@@ -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('取消操作'));
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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' });
|
||||||
// 定义属性
|
// 定义属性
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 }>();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
/** 商品卡片 */
|
/** 商品卡片 */
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
:label="article.title"
|
|
||||||
:value="article.id"
|
|
||||||
/>
|
/>
|
||||||
</ElSelect>
|
</FormItem>
|
||||||
</ElFormItem>
|
</Form>
|
||||||
</ElForm>
|
|
||||||
</ComponentContainerProperty>
|
</ComponentContainerProperty>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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('取消操作'));
|
||||||
|
|||||||
@@ -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('取消操作'));
|
||||||
|
|||||||
@@ -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('取消操作'));
|
||||||
|
|||||||
@@ -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('取消操作'));
|
||||||
|
|||||||
@@ -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('取消操作'));
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 } });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export { default as SummaryCard } from './summary-card.vue';
|
|
||||||
export type { SummaryCardProps } from './typing';
|
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
223
apps/web-ele/src/views/ai/chat/manager/data.ts
Normal file
223
apps/web-ele/src/views/ai/chat/manager/data.ts
Normal 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' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
29
apps/web-ele/src/views/ai/chat/manager/index.vue
Normal file
29
apps/web-ele/src/views/ai/chat/manager/index.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
180
apps/web-ele/src/views/ai/image/manager/data.ts
Normal file
180
apps/web-ele/src/views/ai/image/manager/data.ts
Normal 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' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
116
apps/web-ele/src/views/ai/image/manager/index.vue
Normal file
116
apps/web-ele/src/views/ai/image/manager/index.vue
Normal 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>
|
||||||
99
apps/web-ele/src/views/ai/mindmap/index/index.vue
Normal file
99
apps/web-ele/src/views/ai/mindmap/index/index.vue
Normal 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>
|
||||||
75
apps/web-ele/src/views/ai/mindmap/index/modules/left.vue
Normal file
75
apps/web-ele/src/views/ai/mindmap/index/modules/left.vue
Normal 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>
|
||||||
215
apps/web-ele/src/views/ai/mindmap/index/modules/right.vue
Normal file
215
apps/web-ele/src/views/ai/mindmap/index/modules/right.vue
Normal 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>
|
||||||
97
apps/web-ele/src/views/ai/mindmap/manager/data.ts
Normal file
97
apps/web-ele/src/views/ai/mindmap/manager/data.ts
Normal 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' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
120
apps/web-ele/src/views/ai/mindmap/manager/index.vue
Normal file
120
apps/web-ele/src/views/ai/mindmap/manager/index.vue
Normal 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>
|
||||||
147
apps/web-ele/src/views/ai/model/apiKey/data.ts
Normal file
147
apps/web-ele/src/views/ai/model/apiKey/data.ts
Normal 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' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
128
apps/web-ele/src/views/ai/model/apiKey/index.vue
Normal file
128
apps/web-ele/src/views/ai/model/apiKey/index.vue
Normal 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>
|
||||||
82
apps/web-ele/src/views/ai/model/apiKey/modules/form.vue
Normal file
82
apps/web-ele/src/views/ai/model/apiKey/modules/form.vue
Normal 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>
|
||||||
312
apps/web-ele/src/views/ai/model/chatRole/data.ts
Normal file
312
apps/web-ele/src/views/ai/model/chatRole/data.ts
Normal 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' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
128
apps/web-ele/src/views/ai/model/chatRole/index.vue
Normal file
128
apps/web-ele/src/views/ai/model/chatRole/index.vue
Normal 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
Reference in New Issue
Block a user