v3.9.0 里程碑版本发布

This commit is contained in:
JEECG
2025-11-26 11:25:35 +08:00
parent 1f73837b7d
commit 9919ae2bc5
380 changed files with 11450 additions and 4555 deletions

View File

@ -0,0 +1,139 @@
<template>
<div>
<a-alert
message="多表格实例测试 (Issue #8792 修复)"
description="此示例演示父子组件同时使用 BasicTable 时,列配置、列宽调整等功能正常工作,两个表格实例互不干扰"
type="info"
show-icon
class="mb-4"
/>
<a-card title="父组件的表格" class="mb-4">
<BasicTable
@register="registerParentTable"
:columns="parentColumns"
:dataSource="parentData"
:pagination="false"
showTableSetting
:canResize="false"
>
<template #toolbar>
<a-button type="primary" @click="testParentTable">测试父表格</a-button>
</template>
</BasicTable>
</a-card>
<a-card title="子组件的表格">
<BasicTable
@register="registerChildTable"
:columns="childColumns"
:dataSource="childData"
:pagination="false"
showTableSetting
:canResize="false"
>
<template #toolbar>
<a-button type="primary" @click="testChildTable">测试子表格</a-button>
</template>
</BasicTable>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicTable, useTable, BasicColumn } from '/@/components/Table';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
// 父表格配置
const parentColumns: BasicColumn[] = [
{
title: 'ID',
dataIndex: 'id',
width: 80,
},
{
title: '父表格-姓名',
dataIndex: 'name',
width: 150,
},
{
title: '父表格-年龄',
dataIndex: 'age',
width: 100,
},
{
title: '父表格-地址',
dataIndex: 'address',
width: 200,
},
];
const parentData = ref([
{ id: 1, name: '父表格-张三', age: 25, address: '北京市朝阳区' },
{ id: 2, name: '父表格-李四', age: 30, address: '上海市浦东新区' },
{ id: 3, name: '父表格-王五', age: 28, address: '广州市天河区' },
]);
const [registerParentTable, { getColumns: getParentColumns }] = useTable({
columns: parentColumns,
dataSource: parentData.value,
});
// 子表格配置
const childColumns: BasicColumn[] = [
{
title: 'ID',
dataIndex: 'id',
width: 80,
},
{
title: '子表格-产品名称',
dataIndex: 'product',
width: 150,
},
{
title: '子表格-价格',
dataIndex: 'price',
width: 100,
},
{
title: '子表格-库存',
dataIndex: 'stock',
width: 100,
},
{
title: '子表格-分类',
dataIndex: 'category',
width: 150,
},
];
const childData = ref([
{ id: 1, product: '子表格-商品A', price: 99, stock: 100, category: '电子产品' },
{ id: 2, product: '子表格-商品B', price: 199, stock: 50, category: '家居用品' },
{ id: 3, product: '子表格-商品C', price: 299, stock: 30, category: '服装鞋包' },
]);
const [registerChildTable, { getColumns: getChildColumns }] = useTable({
columns: childColumns,
dataSource: childData.value,
});
function testParentTable() {
const cols = getParentColumns();
createMessage.success(`父表格列数: ${cols.length},请查看控制台`);
console.log('父表格列配置:', cols);
}
function testChildTable() {
const cols = getChildColumns();
createMessage.success(`子表格列数: ${cols.length},请查看控制台`);
console.log('子表格列配置:', cols);
}
</script>
<style scoped>
</style>

View File

@ -12,4 +12,5 @@ export { default as InnerTableDemo } from './InnerTableDemo.vue';
export { default as MergeHeaderDemo } from './MergeHeaderDemo.vue';
export { default as MergeTableDemo } from './MergeTableDemo.vue';
export { default as SelectTableDemo } from './SelectTableDemo.vue';
export { default as TreeTableDemo } from './TreeTableDemo.vue';
export { default as TreeTableDemo } from './TreeTableDemo.vue';
export { default as MultipleTableDemo } from './MultipleTableDemo.vue';

View File

@ -24,9 +24,10 @@
InnerTableDemo,
MergeHeaderDemo,
MergeTableDemo,
SelectTableDemo,
TreeTableDemo,
} from './index';
SelectTableDemo,
TreeTableDemo,
MultipleTableDemo,
} from './index';
export default defineComponent({
name: 'document-table-demo',
components: {
@ -43,9 +44,10 @@
InnerTableDemo,
MergeHeaderDemo,
MergeTableDemo,
SelectTableDemo,
TreeTableDemo,
},
SelectTableDemo,
TreeTableDemo,
MultipleTableDemo,
},
setup() {
//当前选中key
const activeKey = ref('BasicTableDemo');
@ -64,9 +66,10 @@
{ key: 'MergeHeaderDemo', label: '分组表头示例' },
{ key: 'MergeTableDemo', label: '合并行列' },
{ key: 'SelectTableDemo', label: '可选择表格' },
{ key: 'TreeTableDemo', label: '树形表格' },
{ key: 'AuthColumnDemo', label: '权限列设置' },
]);
{ key: 'TreeTableDemo', label: '树形表格' },
{ key: 'AuthColumnDemo', label: '权限列设置' },
{ key: 'MultipleTableDemo', label: '多表格实例' },
]);
//当前选中组件
const currentComponent = computed(() => {
return activeKey.value;

View File

@ -310,11 +310,10 @@
console.log('handleValueChange.event: ', event);
}
// update-begin--author:liaozhiyang---date:20230817---for【issues/636】JVxeTable加上blur事件
// 代码逻辑说明: 【issues/636】JVxeTable加上blur事件
function handleBlur(event){
console.log("blur",event);
}
// update-end--author:liaozhiyang---date:20230817---for【issues/636】JVxeTable加上blur事件
/** 表单验证 */
function handleTableCheck() {
tableRef.value!.validateTable().then((errMap) => {

View File

@ -25,7 +25,7 @@
<JPopup
v-model:value="model[field]"
:formElRef="formElRef"
code="withparamreport"
code="ces_app_rep001"
:param="{ sex: '1' }"
:fieldConfig="[{ source: 'name', target: 'pop2' }]"
/>
@ -34,7 +34,7 @@
<JPopup
v-model:value="model[field]"
:formElRef="formElRef"
code="tj_user_report"
code="report_user"
:param="{ sex: '1' }"
:fieldConfig="[{ source: 'realname', target: 'pop3' }]"
/>

View File

@ -56,9 +56,7 @@
<a @click="handleEdit(record)">编辑</a>
<a-divider type="vertical" />
<a-dropdown>
<!-- update-begin--author:liaozhiyang---date:20230803---forQQYUN-5838图标改小保持一致 -->
<a class="ant-dropdown-link">更多 <Icon icon="mdi-light:chevron-down"></Icon></a>
<!-- update-end--author:liaozhiyang---date:20230803---forQQYUN-5838图标改小保持一致 -->
<template #overlay>
<a-menu class="antd-more">
<a-menu-item>

View File

@ -269,7 +269,7 @@
return props.formDisabled;
});
const emit = defineEmits(['register', 'ok']);
//update-begin---author:wangshuai ---date:20220616 for报表示例验证修改--------------
// 代码逻辑说明: 报表示例验证修改--------------
const formState = reactive<Record<string, any>>({
name: '',
miMa: '',
@ -303,7 +303,6 @@
yuanjia: '',
nyrsfm: '',
});
//update-end---author:wangshuai ---date:20220616 for报表示例验证修改--------------
const { createMessage } = useMessage();
const formRef = ref();
const useForm = Form.useForm;
@ -349,9 +348,8 @@
yuanjia: [{ required: false, message: '请输入数值!' }],
nyrsfm: [{ required: false, message: '请选择年月日时分秒!' }],
};
//update-begin---author:wangshuai ---date:20220616 for报表示例验证修改------------
// 代码逻辑说明: 报表示例验证修改------------
const { resetFields, validate, validateInfos } = useForm(formState, validatorRules, { immediate: false });
//update-end---author:wangshuai ---date:20220616 for报表示例验证修改------------
const ldzjsOptions = ref([
{ label: '男', value: '1' },
{ label: '女', value: '2' },
@ -380,7 +378,7 @@
*/
async function submitForm() {
// 触发表单验证
//update-begin---author:wangshuai ---date:20220616 for报表示例验证修改------------
// 代码逻辑说明: 报表示例验证修改------------
await validate();
confirmLoading.value = true;
let httpurl = '';
@ -425,7 +423,6 @@
.finally(() => {
confirmLoading.value = false;
});
//update-end---author:wangshuai ---date:20220616 for报表示例验证修改--------------
}
/**

View File

@ -72,11 +72,10 @@
const userinfo = userStore.getUserInfo;
userinfo.avatar = data;
userStore.setUserInfo(userinfo);
//update-begin---author:wangshuai ---date:20220909 for[VUEN-2161]用户设置上传头像成功之后直接保存------------
// 代码逻辑说明: [VUEN-2161]用户设置上传头像成功之后直接保存------------
if(data){
defHttp.post({ url: '/sys/user/appEdit', params:{avatar:data} });
}
//update-end---author:wangshuai ---date:20220909 for[VUEN-2161]用户设置上传头像成功之后直接保存--------------
}
/**
*更新基本信息

View File

@ -0,0 +1,252 @@
<template>
<PageWrapper title="EditableCell ID字段测试 (Issue #8924)">
<a-alert
message="🧪 测试目的:验证 beforeEditSubmit 是否会过滤掉 id 字段"
description="请编辑任意单元格并点击对号,查看下方的测试结果。如果 record 中没有 id 字段,说明问题存在。"
type="warning"
show-icon
class="mb-4"
/>
<div class="p-4">
<a-card title="🔬 测试场景1id 字段不在 columns 中(最常见场景)" class="mb-4">
<a-alert
message="⚠️ 核心测试id 在数据中,但不在 columns 中显示"
description="这是最常见的场景:主键字段通常不需要在表格中显示,但在更新数据时必须使用。"
type="info"
show-icon
class="mb-3"
/>
<a-space direction="vertical" style="width: 100%">
<a-card size="small" title="📋 测试数据说明" :bordered="false">
<p><strong>数据源包含</strong>id, name, age, email, address</p>
<p><strong>Columns 显示</strong>name, age, email, address 没有 id </p>
<p><strong>rowKey 配置</strong>'id'</p>
</a-card>
<BasicTable
@register="registerTable1"
:beforeEditSubmit="handleBeforeEditSubmit1"
/>
<a-card
size="small"
:title="testResult1.title"
:bordered="false"
:headStyle="{ backgroundColor: testResult1.bgColor, color: 'white' }"
>
<a-descriptions bordered :column="1" size="small">
<a-descriptions-item label="是否包含 id">
<a-tag :color="testResult1.hasId ? 'success' : 'error'">
{{ testResult1.hasId ? '✅ 包含' : '❌ 不包含' }}
{{ testResult1.hasId ? `(id=${testResult1.idValue})` : '' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="record 包含的字段">
<a-tag v-for="field in testResult1.fields" :key="field" color="blue">{{ field }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="完整 record 内容">
<pre class="test-result-json">{{ testResult1.recordJson }}</pre>
</a-descriptions-item>
<a-descriptions-item label="测试结论">
<a-alert
:message="testResult1.conclusion"
:type="testResult1.hasId ? 'success' : 'error'"
show-icon
/>
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-space>
</a-card>
<a-card title="💡 测试说明" class="mb-4">
<a-space direction="vertical" style="width: 100%">
<a-alert
message="如何进行测试?"
description="1. 点击上方表格任意单元格进行编辑
2. 修改内容后点击对号 ✓ 提交
3. 查看测试结果,观察 record 是否包含 id 字段"
type="info"
show-icon
/>
<a-alert
message="预期结果"
type="success"
show-icon
>
<template #description>
<p><strong>如果代码正常</strong></p>
<p> record 应该包含 id 字段</p>
<p> 可以使用 record.id 进行数据更新</p>
<p> 控制台显示绿色成功消息</p>
</template>
</a-alert>
<a-alert
message="Bug 症状Issue #8924"
type="error"
show-icon
>
<template #description>
<p><strong>如果存在 Bug</strong></p>
<p> record 中没有 id 字段</p>
<p> record 只包含 columns 中定义的字段name, age, email, address</p>
<p> 无法执行数据更新操作</p>
<p> 控制台显示红色错误消息</p>
</template>
</a-alert>
<a-card size="small" title="🔍 原因分析" :bordered="false">
<p>原代码使用 <code>pick(record, keys)</code> 过滤字段</p>
<pre class="code-block">const keys = columns.map(c => c.dataIndex).filter(f => !!f);
// keys = ['name', 'age', 'email', 'address'] // ⚠️ 没有 id
record: pick(record, keys)
// 只保留 keys 中的字段id 被过滤掉了</pre>
</a-card>
</a-space>
</a-card>
</div>
</PageWrapper>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { PageWrapper } from '/@/components/Page';
import { BasicTable, useTable, BasicColumn } from '/@/components/Table';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
// ============ 测试场景1id字段不在columns中 ============
const testResult1 = ref<{
title: string;
bgColor: string;
hasId: boolean;
idValue: any;
fields: string[];
recordJson: string;
conclusion: string;
}>({
title: '📊 测试结果(点击对号后显示)',
bgColor: '#909399',
hasId: false,
idValue: null,
fields: [],
recordJson: '暂无数据,请编辑单元格并点击对号',
conclusion: '等待测试...',
});
const columns1: BasicColumn[] = [
// 注意:这里没有 id 列
{
title: '姓名',
dataIndex: 'name',
width: 150,
edit: true,
editComponent: 'Input',
},
{
title: '年龄',
dataIndex: 'age',
width: 120,
edit: true,
editComponent: 'InputNumber',
},
{
title: '邮箱',
dataIndex: 'email',
width: 200,
edit: true,
editComponent: 'Input',
},
{
title: '地址',
dataIndex: 'address',
width: 200,
edit: true,
editComponent: 'Input',
},
];
const dataSource1 = [
{ id: 1, name: '张三', age: 25, email: 'zhangsan@example.com', address: '北京市朝阳区' },
{ id: 2, name: '李四', age: 30, email: 'lisi@example.com', address: '上海市浦东新区' },
{ id: 3, name: '王五', age: 28, email: 'wangwu@example.com', address: '广州市天河区' },
];
const [registerTable1] = useTable({
rowKey: 'id', // 使用默认id字段作为主键,
columns: columns1,
dataSource: dataSource1,
pagination: false,
showIndexColumn: true,
canResize: false,
});
async function handleBeforeEditSubmit1({ record, index, key, value }) {
console.log('🧪 场景1 测试 - beforeEditSubmit 接收到的数据:', { record, index, key, value });
console.log('🔍 record 详细内容:', JSON.stringify(record, null, 2));
// 分析 record
const hasId = 'id' in record;
const fields = Object.keys(record);
// 更新测试结果
testResult1.value = {
title: hasId ? '✅ 测试通过' : '❌ 测试失败 - 发现 Bug',
bgColor: hasId ? '#67C23A' : '#F56C6C',
hasId: hasId,
idValue: record.id || null,
fields: fields,
recordJson: JSON.stringify(record, null, 2),
conclusion: hasId
? `✅ record 中包含 id 字段(值为 ${record.id}),可以正常更新数据`
: `❌ Bug 确认record 中缺少 id 字段!只包含 ${fields.join(', ')}。这会导致无法执行数据更新操作。`,
};
if (!hasId) {
createMessage.error('❌ 测试失败record 中缺少 id 字段!这就是 Issue #8924 描述的问题。');
console.error('❌ Bug 重现:数据源中有 id但 beforeEditSubmit 收到的 record 中没有 id');
return false;
}
createMessage.success(`✅ 测试通过:获取到 id=${record.id}`);
console.log('✅ 模拟更新请求:', {
url: '/api/user/update',
params: { id: record.id, [key]: value },
});
return true;
}
</script>
<style scoped>
.test-result-json {
font-size: 12px;
line-height: 1.5;
background-color: #f5f5f5;
padding: 8px;
border-radius: 4px;
max-height: 200px;
overflow: auto;
}
.code-block {
font-size: 13px;
line-height: 1.6;
background-color: #282c34;
color: #abb2bf;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
}
p {
margin: 8px 0;
}
</style>

View File

@ -117,13 +117,10 @@
Object.assign(orderMainModel, data.record);
let params = { id: orderMainModel.id };
const customerList = await orderCustomerList(params);
//update-begin---author:wangshuai ---date:20220629 for[VUEN-1484]在一对多示例页面编辑一行青岛订单A0001客户信息无法填入------------
// 代码逻辑说明: [VUEN-1484]在一对多示例页面编辑一行青岛订单A0001客户信息无法填入------------
orderMainModel.jeecgOrderCustomerList = customerList[0]?customerList[0]:{};
//update-end---author:wangshuai ---date:20220629 for[VUEN-1484]在一对多示例页面编辑一行青岛订单A0001客户信息无法填入--------------
const ticketList = await orderTicketList(params);
//update-begin---author:wangshuai ---date:20220629 for[VUEN-1484]在一对多示例页面编辑一行青岛订单A0001客户信息无法填入------------
orderMainModel.jeecgOrderTicketList = ticketList[0]?ticketList[0]:{};
//update-end---author:wangshuai ---date:20220629 for[VUEN-1484]在一对多示例页面编辑一行青岛订单A0001客户信息无法填入--------------
}
});
const getTitle = computed(() => (!unref(isUpdate) ? '新增' : '编辑'));

View File

@ -87,9 +87,8 @@
//注册table数据
const [registerTable, { reload }, { rowSelection }] = tableContext;
//新增类型
//update-begin---author:wangshuai ---date:20220720 for[VUEN-1661]一对多示例,编辑的时候,有时候是一对一,有时候是一对多,默认一对多------------
// 代码逻辑说明: [VUEN-1661]一对多示例,编辑的时候,有时候是一对一,有时候是一对多,默认一对多------------
const addType = ref(3);
//update-end---author:wangshuai ---date:20220720 for[VUEN-1661]一对多示例,编辑的时候,有时候是一对一,有时候是一对多,默认一对多--------------
//添加事件
function handleCreate(e) {
addType.value = e.key;

View File

@ -1,7 +1,6 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit" width="70%" @fullScreen="handleFullScreen">
<a-form ref="formRef" :model="orderMainModel" :label-col="labelCol" :wrapper-col="wrapperCol" :rules="validatorRules">
<!-- update-begin--author:liaozhiyang---date:20230803---forQQYUN-5866鼠标放上去有左右滚动条 -->
<div style="overflow-x: hidden">
<a-row class="form-row" :gutter="16">
<a-col :lg="8">
@ -36,7 +35,6 @@
</a-col>
</a-row>
</div>
<!-- update-end--author:liaozhiyang---date:20230803---forQQYUN-5866鼠标放上去有左右滚动条 -->
<!-- 子表单区域 -->
<a-tabs v-model:activeKey="activeKey" @change="handleChangeTabs">
<a-tab-pane tab="客户信息" key="tableRef1">
@ -176,11 +174,10 @@
//刷新列表
emit('success');
}
// update-begin--author:liaozhiyang---date:20230804---for【QQYUN-5866】放大行数自适应
// 代码逻辑说明: 【QQYUN-5866】放大行数自适应
const handleFullScreen = (val) => {
tableH.value=val ? document.documentElement.clientHeight - 387 : 300;
};
// update-end--author:liaozhiyang---date:20230804---for【QQYUN-5866】放大行数自适应
return {
formRef,
activeKey,

View File

@ -74,6 +74,7 @@
name:"操作日志",
url: getExportUrl,
params: searchInfo,
timeout: 300000, // 设置超时时间为5分钟(300秒)
},
});
@ -82,7 +83,7 @@
// 日志类型
function tabChange(key) {
searchInfo.logType = key;
//update-begin---author:wangshuai ---date:20220506 for[VUEN-943]vue3日志管理列表翻译不对------------
// 代码逻辑说明: [VUEN-943]vue3日志管理列表翻译不对------------
if (key == '2') {
logColumns.value = operationLogColumn;
searchSchema.value = operationSearchFormSchema;
@ -93,7 +94,6 @@
searchSchema.value = searchFormSchema;
logColumns.value = columns;
}
//update-end---author:wangshuai ---date:20220506 for[VUEN-943]vue3日志管理列表翻译不对--------------
reload();
}

View File

@ -11,10 +11,13 @@
:destroyOnClose="true"
@visible-change="handleVisibleChange"
>
<div class="print-btn" @click="onPrinter">
<Icon icon="ant-design:printer-filled" />
<span class="print-text">打印</span>
</div>
<template #title>
<span class="basic-title">查看详情</span>
<div class="print-btn" @click="onPrinter">
<Icon icon="ant-design:printer-filled" />
<span class="print-text">打印</span>
</div>
</template>
<a-card class="daily-article">
<a-card-meta :title="content.titile">
<template #description>
@ -76,6 +79,7 @@
import { useGlobSetting } from '@/hooks/setting';
import { encryptByBase64 } from '@/utils/cipher';
import { getToken } from '@/utils/auth';
import {defHttp} from "@/utils/http/axios";
const router = useRouter();
const glob = useGlobSetting();
const isUpdate = ref(true);
@ -92,21 +96,22 @@
noticeFiles.value = [];
if (unref(isUpdate)) {
//data.record.msgContent = '<p>2323</p><input onmouseover=alert(1)>xss test';
//update-begin-author:taoyan date:2022-7-14 for: VUEN-1702 【禁止问题】sql注入漏洞
// 代码逻辑说明: VUEN-1702 【禁止问题】sql注入漏洞
if (data.record.msgContent) {
//update-begin---author:wangshuai---date:2023-11-15---for:【QQYUN-7049】3.6.0版本 通知公告中发布的富文本消息,在我的消息中查看没有样式---
// 代码逻辑说明: 【QQYUN-7049】3.6.0版本 通知公告中发布的富文本消息,在我的消息中查看没有样式---
data.record.msgContent = xss(data.record.msgContent, options);
//update-end---author:wangshuai---date:2023-11-15---for:【QQYUN-7049】3.6.0版本 通知公告中发布的富文本消息,在我的消息中查看没有样式---
}
//update-end-author:taoyan date:2022-7-14 for: VUEN-1702 【禁止问题】sql注入漏洞
//update-begin-author:liusq---date:2025-06-17--for: [QQYUN-12521]通知公告消息增加访问量
// 代码逻辑说明: [QQYUN-12521]通知公告消息增加访问量
if (!data.record?.busId) {
await addVisitsNum({ id: data.record.id });
}
//update-end-author:liusq---date:2025-06-17--for: [QQYUN-12521]通知公告消息增加访问量
content.value = data.record;
if(content.value.sender){
const userInfo = await defHttp.get({ url: '/sys/user/queryUserComponentData?isMultiTranslate=true', params: { username: content.value.sender } });
content.value.sender = userInfo && userInfo?.records && userInfo?.records.length>0?userInfo.records[0].realname : content.value.sender;
}
console.log('data---------->>>', data);
if (data.record?.files && data.record?.files.length > 0) {
noticeFiles.value = data.record.files.split(',').map((item) => {
@ -300,13 +305,14 @@
.print-btn {
position: absolute;
top: 80px;
right: 40px;
right: 100px;
top: 20px;
cursor: pointer;
color: #a3a3a5;
z-index: 999;
.print-text {
margin-left: 5px;
font-size: 14px;
}
&:hover {
color: #40a9ff;
@ -364,4 +370,17 @@
max-width: 100%;
height: auto;
}
.basic-title{
position: relative;
display: flex;
padding-left: 7px;
font-size: 16px;
font-weight: 500;
line-height: 24px;
color: rgba(0,0,0,0.88);
cursor: move;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
</style>

View File

@ -1,21 +1,38 @@
<template>
<div>
<BasicTable @register="registerTable" :searchInfo="searchInfo">
<BasicTable @register="registerTable" :searchInfo="searchInfo" :rowSelection="rowSelection">
<template #tableTitle>
<a-button type="primary" @click="handlerReadAllMsg">全部标注已读</a-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button>
批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getActions(record)" />
</template>
</BasicTable>
<DetailModal @register="register" />
<keep-alive>
<component v-if="currentModal" v-bind="bindParams" :key="currentModal" :is="currentModal" @register="modalRegCache[currentModal].register" />
</keep-alive>
</div>
</template>
<script lang="ts" name="monitor-mynews" setup>
import { ref, onMounted } from 'vue';
import {ref, onMounted, unref} from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import DetailModal from './DetailModal.vue';
import { getMyNewsList, editCementSend, syncNotic, readAllMsg, getOne } from './mynews.api';
import { getMyNewsList, editCementSend, syncNotic, readAllMsg, getOne, deleteAnnSend, deleteBatchAnnSend } from './mynews.api';
import { columns, searchFormSchema } from './mynews.data';
import { useMessage } from '/@/hooks/web/useMessage';
import { getToken } from '/@/utils/auth';
@ -35,18 +52,19 @@
const appStore = useAppStore();
const router = useRouter();
const { currentRoute } = useRouter();
const { goPage } = useMessageHref();
// update-begin--author:liaozhiyang---date:20250709---for【QQYUN-13058】我的消息区分类型且支持根据url参数查询类型
const { goPage, currentModal, modalRegCache, bindParams } = useMessageHref();
// 代码逻辑说明: 【QQYUN-13058】我的消息区分类型且支持根据url参数查询类型
const querystring = currentRoute.value.query;
const findItem: any = searchFormSchema.find((item: any) => item.field === 'msgCategory');
if (findItem) {
if (querystring?.msgCategory) {
findItem.componentProps.defaultValue = querystring.msgCategory
} else if (querystring.noticeType) {
findItem.componentProps.defaultValue = querystring.noticeType;
} else {
findItem.componentProps.defaultValue = null
}
}
// update-end--author:liaozhiyang---date:20250709---for【QQYUN-13058】我的消息区分类型且支持根据url参数查询类型
const { prefixCls, tableContext } = useListPage({
designScope: 'mynews-list',
tableProps: {
@ -55,21 +73,30 @@
columns: columns,
formConfig: {
schemas: searchFormSchema,
//update-begin---author:wangshuai---date:2024-06-11---for:【TV360X-545】我的消息列表不能通过时间范围查询---
// 代码逻辑说明: 【TV360X-545】我的消息列表不能通过时间范围查询---
fieldMapToTime: [['sendTime', ['sendTimeBegin', 'sendTimeEnd'], 'YYYY-MM-DD']],
//update-end---author:wangshuai---date:2024-06-11---for:【TV360X-545】我的消息列表不能通过时间范围查询---
},
beforeFetch: (params) => {
// update-begin--author:liaozhiyang---date:20250709---for【QQYUN-13058】我的消息区分类型且支持根据url参数查询类型
if (querystring?.msgCategory) {
params.msgCategory = querystring.msgCategory;
// 代码逻辑说明: 【QQYUN-13058】我的消息区分类型且支持根据url参数查询类型
if (params.msgCategory) {
if (['1', '2'].includes(params.msgCategory)) {
params.msgCategory = params.msgCategory;
} else {
params.noticeType = params.msgCategory;
delete params.msgCategory;
}
} else {
if (querystring?.msgCategory) {
params.msgCategory = querystring.msgCategory;
} else if (querystring.noticeType) {
params.noticeType = querystring.noticeType;
}
}
return params;
// update-end--author:liaozhiyang---date:20250709---for【QQYUN-13058】我的消息区分类型且支持根据url参数查询类型
},
},
});
const [registerTable, { reload }] = tableContext;
const [registerTable, { reload }, { rowSelection, selectedRows, selectedRowKeys }] = tableContext;
/**
* 操作列定义
* @param record
@ -80,6 +107,14 @@
label: '查看',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record.id),
},
ifShow: record.readFlag === 1
}
];
}
@ -99,7 +134,7 @@
});
}
goPage(record, openModalFun);
}
// 日志类型
function callback(key) {
@ -118,8 +153,8 @@
function onSelectChange(selectedRowKeys: (string | number)[]) {
checkedKeys.value = selectedRowKeys;
}
//update-begin-author:taoyan date:2022-8-23 for: 消息跳转,打开详情表单
// 代码逻辑说明: 消息跳转,打开详情表单
onMounted(()=>{
initHrefModal();
});
@ -146,7 +181,30 @@
}
}
}
//update-end-author:taoyan date:2022-8-23 for: 消息跳转,打开详情表单
function handleSuccess() {
selectedRowKeys.value = [];
reload();
}
/**
* 删除我的消息
*
* @param id
*/
async function handleDelete(id) {
await deleteAnnSend({ id: id }, handleSuccess);
}
/**
* 批量删除我的消息
*/
async function batchHandleDelete() {
let unRead = unref(selectedRows).filter((item) => item.readFlag == 0);
if (unref(unRead).length > 0) {
createMessage.warning('未阅读的消息禁止删除!');
return;
}
await deleteBatchAnnSend({ ids: selectedRowKeys.value }, handleSuccess);
}
</script>

View File

@ -7,6 +7,8 @@ enum Api {
readAllMsg = '/sys/sysAnnouncementSend/readAll',
syncNotic = '/sys/annountCement/syncNotic',
getOne = '/sys/sysAnnouncementSend/getOne',
delete = '/sys/sysAnnouncementSend/delete',
deleteBatch = '/sys/sysAnnouncementSend/deleteBatch',
}
/**
@ -59,3 +61,34 @@ export const getOne = (sendId) => {
return defHttp.get({ url: Api.getOne, params:{sendId} });
};
/**
* 删除用户通告阅读标记的数据
* @param params
* @param handleSuccess
*/
export const deleteAnnSend = (params, handleSuccess) =>{
return defHttp.delete({ url: Api.delete, params }, { joinParamsToUrl: true }).then(()=>{
handleSuccess();
})
}
/**
* 批量删除用户通告阅读标记的数据
* @param params
* @param handleSuccess
*/
export const deleteBatchAnnSend = (params, handleSuccess) =>{
Modal.confirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, params }, { joinParamsToUrl: true }).then(()=>{
handleSuccess();
})
},
});
}

View File

@ -89,6 +89,12 @@ export const searchFormSchema: FormSchema[] = [
options: [
{ label: '通知公告', value: '1' },
{ label: '系统消息', value: '2' },
{ label: '日程计划', value: 'plan' },
{ label: '流程消息', value: 'flow' },
{ label: '会议', value: 'meeting' },
{ label: '知识库', value: 'file' },
{ label: '协同通知', value: 'collab' },
{ label: '督办通知', value: 'supe' },
],
},
colProps: { span: 6 },

View File

@ -18,9 +18,8 @@
// labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
// update-begin--author:liaozhiyang---date:20231017---for【issues/790】弹窗内文本框不居中问题
// 代码逻辑说明: 【issues/790】弹窗内文本框不居中问题
labelWidth: 100,
// update-end--author:liaozhiyang---date:20231017---for【issues/790】弹窗内文本框不居中问题
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {

View File

@ -203,9 +203,8 @@
initCharts();
openTimer();
});
// update-begin--author:liaozhiyang---date:220230719---for【issues-615】系统监控中的REDIS监控页面打开再关闭后没有关闭计时器
// 代码逻辑说明: 【issues-615】系统监控中的REDIS监控页面打开再关闭后没有关闭计时器
onUnmounted(() => {
closeTimer();
});
// update-end--author:liaozhiyang---date:220230719---for【issues-615】系统监控中的REDIS监控页面打开再关闭后没有关闭计时器
</script>

View File

@ -112,12 +112,10 @@
<DeleteOutlined size="22" @click="removeFilter(router, index)" />
</a-divider>
<div v-for="(tag, index) in item.args" :key="tag.key">
<!-- update-begin---author:wangshuai ---date: 20230829 forvue3.0后自定义表单重复组件要用a-form-item-rest,否则会警告提醒------------ -->
<a-form-item-rest>
<a-input v-model:value="tag.key" placeholder="参数键" style="width: 45%; margin-right: 8px" />
<a-input v-model:value="tag.value" placeholder="参数值" style="width: 40%; margin-right: 8px; margin-top: 3px" />
</a-form-item-rest>
<!-- update-end---author:wangshuai ---date: 20230829 forvue3.0后自定义表单重复组件要用a-form-item-rest,否则会警告提醒------------ -->
<CloseOutlined :size="22" @click="removeFilterParams(item, index)" />
</div>
<a-button type="dashed" style="margin-left: 28%; width: 37%; margin-top: 5px" size="small" @click="addFilterParams(item)">

View File

@ -11,9 +11,7 @@
</a-tab-pane>
<a-tab-pane key="5" tab="内存信息" />
</a-tabs>
<!-- update-begin---author:wangshuai ---date: 20230829 for性能监控切换到磁盘监控再切回来报错列为空不能用if判断------------>
<BasicTable @register="registerTable" :searchInfo="searchInfo" :dataSource="dataSource" v-show="activeKey != 4">
<!-- update-end---author:wangshuai ---date: 20230829 for性能监控切换到磁盘监控再切回来报错列为空不能用if判断------------>
<template #tableTitle>
<div slot="message"
>上次更新时间{{ lastUpdateTime }}

View File

@ -31,8 +31,20 @@
:chatTitle="chatTitle"
:quickCommandData="quickCommandData"
:showAdvertising = "showAdvertising"
:hasExtraFlowInputs="hasExtraFlowInputs"
:conversationSettings="getCurrentSettings"
@edit-settings="handleEditSettings"
ref="chatRef"
></chat>
</div>
<!-- [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程 -->
<ConversationSettingsModal
ref="settingsModalRef"
:flowInputs="flowInputs"
:conversationId="uuid"
:existingSettings="getCurrentSettings"
@ok="handleSettingsOk"
/>
</template>
<Loading :loading="loading" tip="加载中,请稍后"></Loading>
</div>
@ -41,8 +53,9 @@
<script setup lang="ts">
import slide from './slide.vue';
import chat from './chat.vue';
import { Spin } from 'ant-design-vue';
import { ref, watch, nextTick, onUnmounted, onMounted } from 'vue';
import ConversationSettingsModal from './components/ConversationSettingsModal.vue';
import { Spin, message } from 'ant-design-vue';
import { ref, watch, nextTick, onUnmounted, onMounted, computed } from 'vue';
import { useUserStore } from '/@/store/modules/user';
import { JEECG_CHAT_KEY } from '/@/enums/cacheEnum';
import { defHttp } from '/@/utils/http/axios';
@ -84,6 +97,12 @@
const quickCommandData = ref<any>([]);
//是否显示广告位
const showAdvertising = ref<boolean>(false);
//对话设置弹窗ref
const settingsModalRef = ref();
//工作流入参列表
const flowInputs = ref<any[]>([]);
//当前会话的设置
const conversationSettings = ref<Record<string, Record<string, any>>>({});
const priming = () => {
dataSource.value = {
@ -104,6 +123,99 @@
}, 50);
};
/**
* 检查是否有额外的工作流入参
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
*/
const hasExtraFlowInputs = computed(() => {
if (!appData.value || !appData.value.metadata) {
return false;
}
try {
const metadata = typeof appData.value.metadata === 'string'
? JSON.parse(appData.value.metadata)
: appData.value.metadata;
const flowInputsList = metadata.flowInputs || [];
// 过滤掉固定参数
const fixedParams = ['history', 'content', 'images'];
const extraInputs = flowInputsList.filter((input: any) => !fixedParams.includes(input.field));
return extraInputs.length > 0;
} catch (e) {
console.error('解析metadata失败', e);
return false;
}
});
// 检查是否有必填的额外参数
const hasRequiredFlowInputs = computed(() => {
if (!appData.value || !appData.value.metadata) {
return false;
}
try {
const metadata = typeof appData.value.metadata === 'string'
? JSON.parse(appData.value.metadata)
: appData.value.metadata;
const flowInputsList = metadata.flowInputs || [];
// 过滤掉固定参数,且必须是必填的
const fixedParams = ['history', 'content', 'images'];
const requiredInputs = flowInputsList.filter((input: any) =>
!fixedParams.includes(input.field) && input.required
);
return requiredInputs.length > 0;
} catch (e) {
console.error('解析metadata失败', e);
return false;
}
});
// 监听appData变化更新flowInputs
watch(
() => appData.value,
(val) => {
if (!val || !val.metadata) {
flowInputs.value = [];
return;
}
try {
const metadata = typeof val.metadata === 'string'
? JSON.parse(val.metadata)
: val.metadata;
flowInputs.value = metadata.flowInputs || [];
} catch (e) {
console.error('解析metadata失败', e);
flowInputs.value = [];
}
},
{ immediate: true, deep: true }
);
// 获取当前会话的设置
const getCurrentSettings = computed(() => {
return conversationSettings.value[uuid.value] || {};
});
// 编辑对话设置
function handleEditSettings() {
if (settingsModalRef.value) {
settingsModalRef.value.open();
}
}
// 保存对话设置
function handleSettingsOk(data: Record<string, any>) {
// 保存到本地状态(会在发送消息时传给后端)
conversationSettings.value[uuid.value] = data;
message.success('对话设置已保存');
nextTick(() => {
chatVisible.value = true;
});
}
// 监听dataSource变化执行操作
const execute = () => {
unwatch01 = watch(
@ -117,6 +229,12 @@
chatVisible.value = false;
nextTick(() => {
chatVisible.value = true;
// 新会话且有必填参数,弹出设置弹窗
if (hasRequiredFlowInputs.value && !conversationSettings.value['1002']) {
if (settingsModalRef.value) {
settingsModalRef.value.open();
}
}
});
return;
}
@ -131,14 +249,33 @@
uuid.value = value;
defHttp.get({ url: '/airag/chat/messages', params }, { isTransformResponse: false }).then((res) => {
if (res.success) {
chatData.value = res.result;
// 处理新的返回格式包含messages和flowInputs
if (res.result && res.result.messages) {
chatData.value = res.result.messages;
// 加载已保存的设置
if (res.result.flowInputs) {
conversationSettings.value[value] = res.result.flowInputs;
}
} else if (Array.isArray(res.result)) {
// 兼容旧格式
chatData.value = res.result;
} else {
chatData.value = [];
}
} else {
chatData.value = [];
}
chatVisible.value = false;
nextTick(() => {
chatVisible.value = true;
});
// 新会话且有必填参数,弹出设置弹窗
if (hasRequiredFlowInputs.value && !conversationSettings.value[value]) {
if (settingsModalRef.value) {
settingsModalRef.value.open();
}
}else{
nextTick(() => {
chatVisible.value = true;
});
}
});
}else{
chatData.value = [];

View File

@ -0,0 +1,326 @@
<template>
<div v-if="text != ''" class="textWrap" :class="[inversion === 'user' ? 'self' : 'chatgpt']" ref="textRef">
<div :style="{ width: getIsMobile ? screenWidth : 'auto' }">
<div class="markdown-body" :class="{ 'markdown-body-generate': loading }" :style="{ color: error ? '#FF4444 !important' : '' }" v-html="text" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue';
import MarkdownIt from 'markdown-it';
import './style/github-markdown.less';
import './style/highlight.less';
import './style/style.less';
import { useAppInject } from '@/hooks/web/useAppInject';
import hljs from 'highlight.js';
import mila from 'markdown-it-link-attributes';
import mdKatex from '@traptitech/markdown-it-katex';
import { useGlobSetting } from '@/hooks/setting';
/**
* 屏幕宽度
*/
const screenWidth = ref<string>();
const { getIsMobile } = useAppInject();
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading', 'referenceKnowledge']);
const textRef = ref();
const mdi = new MarkdownIt({
html: true,
linkify: true,
highlight(code, language) {
const validLang = !!(language && hljs.getLanguage(language));
if (validLang) {
const lang = language ?? '';
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang);
}
return highlightBlock(hljs.highlightAuto(code).value, '');
},
});
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } });
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' });
const text = computed(() => {
let value = props.text ?? '';
if (props.inversion != 'user') {
value = replaceImageWith(value);
value = replaceDomainUrl(value);
return mdi.render(value);
}
return value.replace('\n', '<br>');
});
// 是否显示引用知识库
const showRefKnow = computed(() => {
const { loading, referenceKnowledge } = props;
if (loading) {
return false;
}
return Array.isArray(referenceKnowledge) && referenceKnowledge.length > 0;
});
//替换图片宽度
const replaceImageWith = (markdownContent) => {
// 支持图片设置width的写法 ![](/static/jimuImages/screenshot_1617252560523.png =100)
const regex = /!\[([^\]]*)\]\(([^)]+)=([0-9]+)\)/g;
return markdownContent.replace(regex, (match, alt, src, width) => {
let reg = /#\s*{\s*domainURL\s*}/g;
src = src.replace(reg, domainUrl);
return `<div><img src='${src}' alt='${alt}' width='${width}' /></div>`;
});
};
const { domainUrl } = useGlobSetting();
//替换domainURL
const replaceDomainUrl = (markdownContent) => {
const regex = /!\[([^\]]*)\]\(.*?#\s*{\s*domainURL\s*}.*?\)/g;
return markdownContent.replace(regex, (match) => {
let reg = /#\s*{\s*domainURL\s*}/g;
return match.replace(reg, domainUrl);
});
};
//是否放大图片
const amplifyImage = ref<boolean>(false);
//图片地址
const imageUrl = ref<string>('');
function highlightBlock(str: string, lang?: string) {
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">复制代码</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`;
}
function addCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy');
copyBtn.forEach((btn) => {
btn.addEventListener('click', () => {
const code = btn.parentElement?.nextElementSibling?.textContent;
if (code) {
copyToClip(code).then(() => {
btn.textContent = '复制成功';
setTimeout(() => {
btn.textContent = '复制代码';
}, 1e3);
});
}
});
});
}
}
function removeCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy');
copyBtn.forEach((btn) => {
btn.removeEventListener('click', () => {});
});
}
}
/**
* 添加图片点击事件
*/
function addImageClickEvent() {
if (textRef.value) {
const image = textRef.value.querySelectorAll('img');
image.forEach((img) => {
img.addEventListener('click', () => {
imageUrl.value = img.src;
amplifyImage.value = true;
});
});
}
}
/**
* 移出图片点击事件
*/
function removeImageClickEvent() {
if (textRef.value) {
const image = textRef.value.querySelectorAll('img');
image.forEach((img) => {
img.removeEventListener('click', () => {});
});
}
}
/**
* 图片隐藏
*/
function pictureHide() {
amplifyImage.value = false;
imageUrl.value = '';
}
/**
* 设置markdown body整体宽度
*/
function setMarkdownBodyWidth() {
//平板
console.log('window.innerWidth::', window.innerWidth);
if (window.innerWidth > 600 && window.innerWidth < 1024) {
screenWidth.value = window.innerWidth - 120 + 'px';
} else if (window.innerWidth < 600) {
//手机
screenWidth.value = window.innerWidth - 60 + 'px';
}
}
onMounted(() => {
addCopyEvents();
addImageClickEvent();
setMarkdownBodyWidth();
window.addEventListener('resize', setMarkdownBodyWidth);
});
onUpdated(() => {
addCopyEvents();
addImageClickEvent();
});
onUnmounted(() => {
removeCopyEvents();
removeImageClickEvent();
window.removeEventListener('resize', setMarkdownBodyWidth);
});
function copyToClip(text: string) {
return new Promise((resolve, reject) => {
try {
const input: HTMLTextAreaElement = document.createElement('textarea');
input.setAttribute('readonly', 'readonly');
input.value = text;
document.body.appendChild(input);
input.select();
if (document.execCommand('copy')) document.execCommand('copy');
document.body.removeChild(input);
resolve(text);
} catch (error) {
reject(error);
}
});
}
</script>
<style lang="less" scoped>
.textWrap {
border-radius: 0.375rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
font-size: 0.875rem;
line-height: 1.25rem;
border-left: 1px solid #e1e5ea;
}
.error {
background: linear-gradient(135deg, #ff4444, #ff914d) !important;
border-radius: 0.375rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
font-size: 0.875rem;
line-height: 1.25rem;
}
.self {
// background-color: #d2f9d1;
background-color: @primary-color;
color: #fff;
overflow-wrap: break-word;
line-height: 1.625;
min-width: 20px;
}
.chatgpt {
font-size: 0.875rem;
line-height: 1.25rem;
}
// 已停止下方的样式
:deep(.markdown-body) {
color: #333;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial,
sans-serif;
line-height: 1.6;
padding: 14px;
// 段落样式
p {
color: #333;
line-height: 1.6;
margin-bottom: 1em;
margin-top: 0;
}
// 列表样式
ul,
ol {
margin-left: 1.5em;
margin-bottom: 1em;
padding-left: 0;
li {
color: #333;
line-height: 1.6;
margin-bottom: 0.5em;
padding-left: 0.5em;
}
}
// 关键词/代码高亮样式
code {
background-color: #f0f0f0;
color: #333;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.9em;
}
// 行内代码
p code,
li code,
td code {
background-color: #f0f0f0;
color: #333;
padding: 2px 6px;
border-radius: 3px;
}
// 代码块保持原有样式
pre {
code {
background-color: transparent;
padding: 0;
}
}
// 强调文本
strong,
b {
color: #333;
font-weight: 600;
}
// 链接样式
a {
color: #4183c4;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
@media (max-width: 1024px) {
//手机和平板下的样式
.textWrap {
margin-left: -40px;
margin-top: 10px;
}
}
</style>

View File

@ -2,13 +2,26 @@
<div class="chatWrap">
<div class="content">
<div class="header-title" v-if="type === 'view' && headerTitle">
{{headerTitle}}
<div v-if="showAdvertising" class="header-advertisint">
AI客服由
<a style="color: #4183c4;margin-left: 2px;margin-right: 2px" href="https://www.qiaoqiaoyun.com/aiCustomerService" target="_blank">
敲敲云
</a>
提供
<div class="title-content">
<span>{{headerTitle}}</span>
<a-button
v-if="hasExtraFlowInputs"
type="text"
class="edit-btn"
@click="handleEditSettings"
title="参数设置"
>
<Icon icon="ant-design:setting-outlined" :size="16" />
</a-button>
</div>
<div class="header-actions">
<div v-if="showAdvertising" class="header-advertisint">
AI客服由
<a style="color: #4183c4;margin-left: 2px;margin-right: 2px" href="https://www.qiaoqiaoyun.com/aiCustomerService" target="_blank">
敲敲云
</a>
提供
</div>
</div>
</div>
<div class="main">
@ -28,6 +41,8 @@
:images = "item.images"
:retrievalText="item.retrievalText"
:referenceKnowledge="item.referenceKnowledge"
:eventType="item.eventType"
:showAvatar="item.showAvatar"
@send="handleOutQuestion"
></chatMessage>
</div>
@ -36,7 +51,7 @@
</div>
<div class="footer">
<div class="topArea">
<presetQuestion @outQuestion="handleOutQuestion" :quickCommandData="quickCommandData"></presetQuestion>
<presetQuestion @out-question="handleOutQuestion" :quickCommandData="quickCommandData"></presetQuestion>
</div>
<div class="bottomArea">
<a-button type="text" class="delBtn" @click="handleDelSession()">
@ -54,7 +69,7 @@
d="M816.872727 158.254545h-181.527272V139.636364c0-39.563636-30.254545-69.818182-69.818182-69.818182h-107.054546c-39.563636 0-69.818182 30.254545-69.818182 69.818182v18.618181H207.127273c-48.872727 0-90.763636 41.890909-90.763637 93.09091s41.890909 90.763636 90.763637 90.763636h609.745454c51.2 0 90.763636-41.890909 90.763637-90.763636 0-51.2-41.890909-93.090909-90.763637-93.09091zM435.2 139.636364c0-13.963636 9.309091-23.272727 23.272727-23.272728h107.054546c13.963636 0 23.272727 9.309091 23.272727 23.272728v18.618181h-153.6V139.636364z m381.672727 155.927272H207.127273c-25.6 0-44.218182-20.945455-44.218182-44.218181 0-25.6 20.945455-44.218182 44.218182-44.218182h609.745454c25.6 0 44.218182 20.945455 44.218182 44.218182 0 23.272727-20.945455 44.218182-44.218182 44.218181zM835.490909 407.272727h-121.018182c-13.963636 0-23.272727 9.309091-23.272727 23.272728s9.309091 23.272727 23.272727 23.272727h97.745455V837.818182c0 39.563636-30.254545 69.818182-69.818182 69.818182h-37.236364V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-118.690909V602.763636c0-13.963636-9.309091-23.272727-23.272728-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364H372.363636V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-34.909091c-39.563636 0-69.818182-30.254545-69.818182-69.818182V453.818182H558.545455c13.963636 0 23.272727-9.309091 23.272727-23.272727s-9.309091-23.272727-23.272727-23.272728H197.818182c-13.963636 0-23.272727 9.309091-23.272727 23.272728V837.818182c0 65.163636 51.2 116.363636 116.363636 116.363636h451.490909c65.163636 0 116.363636-51.2 116.363636-116.363636V430.545455c0-13.963636-11.636364-23.272727-23.272727-23.272728z"
fill="currentColor"
p-id="1585"
></path>
/>
</svg>
</a-button>
<a-button v-if="type === 'view'" type="text" class="contextBtn" :class="[usingContext && 'enabled']" @click="handleUsingContext">
@ -71,7 +86,7 @@
<path
fill="currentColor"
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.956 9.956 0 0 1-4.708-1.175L2 22l1.176-5.29A9.956 9.956 0 0 1 2 12C2 6.477 6.477 2 12 2m0 2a8 8 0 0 0-8 8c0 1.335.326 2.618.94 3.766l.35.654l-.656 2.946l2.948-.654l.653.349A7.955 7.955 0 0 0 12 20a8 8 0 1 0 0-16m1 3v5h4v2h-6V7z"
></path>
/>
</svg>
</a-button>
<div class="chat-textarea" :class="textareaActive?'textarea-active':''">
@ -89,7 +104,7 @@
v-model:value="prompt"
:autoSize="{ minRows: 1, maxRows: 6 }"
:placeholder="placeholder"
@pressEnter="handleEnter"
@press-enter="handleEnter"
@focus="textareaActive = true"
@blur="textareaActive = false"
autofocus
@ -113,10 +128,20 @@
d="M512 967.111111c-250.311111 0-455.111111-204.8-455.111111-455.111111s204.8-455.111111 455.111111-455.111111 455.111111 204.8 455.111111 455.111111-204.8 455.111111-455.111111 455.111111z m0-56.888889c221.866667 0 398.222222-176.355556 398.222222-398.222222s-176.355556-398.222222-398.222222-398.222222-398.222222 176.355556-398.222222 398.222222 176.355556 398.222222 398.222222 398.222222z"
fill="currentColor"
p-id="5215"
></path>
<path d="M341.333333 341.333333h341.333334v341.333334H341.333333z" fill="currentColor" p-id="5216"></path>
/>
<path d="M341.333333 341.333333h341.333334v341.333334H341.333333z" fill="currentColor" p-id="5216"/>
</svg>
</a-button>
<a-tooltip v-if="!loading && showWebSearch" :title="enableSearch ? '关闭联网搜索' : '开启联网搜索'">
<a-button
class="sendBtn webSearchBtn"
type="text"
:class="{'enabled': enableSearch}"
@click="toggleWebSearch"
>
<Icon icon="ant-design:global-outlined" :style="enableSearch ? {color: '#52c41a'} : {color: '#3d4353'}"></Icon>
</a-button>
</a-tooltip>
<a-upload
accept=".jpg,.jpeg,.png"
v-if="!loading"
@ -162,7 +187,7 @@
d="M865.28 202.5472c-17.1008-15.2576-41.0624-19.6608-62.5664-11.5712L177.7664 427.1104c-23.2448 8.8064-38.5024 29.696-39.6288 54.5792-1.1264 24.8832 11.9808 47.104 34.4064 58.0608l97.5872 47.7184c4.5056 2.2528 8.0896 6.0416 9.9328 10.6496l65.4336 161.1776c7.7824 19.1488 24.4736 32.9728 44.7488 37.0688 20.2752 4.096 41.0624-2.1504 55.6032-16.7936l36.352-36.352c6.4512-6.4512 16.5888-7.8848 24.576-3.3792l156.5696 88.8832c9.4208 5.3248 19.8656 8.0896 30.3104 8.0896 8.192 0 16.4864-1.6384 24.2688-5.0176 17.8176-7.68 30.72-22.8352 35.4304-41.6768l130.7648-527.1552c5.5296-22.016-1.7408-45.2608-18.8416-60.416z m-20.8896 50.7904L713.5232 780.4928c-1.536 6.2464-5.8368 11.3664-11.776 13.9264s-12.5952 2.1504-18.2272-1.024L526.9504 704.512c-9.4208-5.3248-19.8656-7.9872-30.208-7.9872-15.9744 0-31.744 6.144-43.52 17.92l-36.352 36.352c-3.8912 3.8912-8.9088 5.9392-14.2336 6.0416l55.6032-152.1664c0.512-1.3312 1.2288-2.56 2.2528-3.6864l240.3328-246.1696c8.2944-8.4992-2.048-21.9136-12.3904-16.0768L301.6704 559.8208c-4.096-3.584-8.704-6.656-13.6192-9.1136L190.464 502.9888c-11.264-5.5296-11.5712-16.1792-11.4688-19.3536 0.1024-3.1744 1.536-13.824 13.2096-18.2272L817.152 229.2736c10.4448-3.9936 18.0224 1.3312 20.8896 3.8912 2.8672 2.4576 9.0112 9.3184 6.3488 20.1728z"
p-id="4238"
fill="currentColor"
></path>
/>
</svg>
</a-button>
</div>
@ -175,7 +200,7 @@
<script setup lang="ts">
import { Ref, watch } from 'vue';
import { computed, ref, createVNode, onUnmounted, onMounted } from 'vue';
import { computed, ref, createVNode, onUnmounted, onMounted, nextTick } from 'vue';
import { useScroll } from './js/useScroll';
import chatMessage from './chatMessage.vue';
import presetQuestion from './presetQuestion.vue';
@ -191,13 +216,14 @@
import { createImgPreview } from "@/components/Preview";
import { useAppInject } from "@/hooks/web/useAppInject";
import { useGlobSetting } from "@/hooks/setting";
import { Icon } from '/@/components/Icon';
message.config({
prefixCls: 'ai-chat-message',
});
const props = defineProps(['uuid', 'prologue', 'formState', 'url', 'type','historyData','chatTitle','presetQuestion','quickCommandData','showAdvertising']);
const emit = defineEmits(['save','reload-message-title']);
const props = defineProps(['uuid', 'prologue', 'formState', 'url', 'type','historyData','chatTitle','presetQuestion','quickCommandData','showAdvertising','hasExtraFlowInputs','conversationSettings']);
const emit = defineEmits(['save','reload-message-title','edit-settings']);
const { scrollRef, scrollToBottom } = useScroll();
const prompt = ref<string>('');
const loading = ref<boolean>(false);
@ -230,6 +256,16 @@
const globSetting = useGlobSetting();
const baseUploadUrl = globSetting.uploadUrl;
const uploadUrl = ref<string>(`${baseUploadUrl}/airag/chat/upload`);
//是否为断线重连
const isReConnect = ref<boolean>(false);
//是否存在思考过程
const isThinking = ref<boolean>(false);
//是否开启网络搜索
const enableSearch = ref<boolean>(false);
//是否显示网络搜索按钮(只有千问模型支持)
const showWebSearch = ref<boolean>(false);
//模型provider信息
const modelProvider = ref<string>('');
function handleEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
@ -238,15 +274,15 @@
}
}
function handleSubmit() {
let message = prompt.value;
if (!message || message.trim() === '') return;
let userMessage = prompt.value;
if (!userMessage || userMessage.trim() === '') return;
prompt.value = '';
onConversation(message);
onConversation(userMessage);
}
const handleOutQuestion = (message) => {
onConversation(message);
const handleOutQuestion = (userMessage) => {
onConversation(userMessage);
};
async function onConversation(message) {
async function onConversation(userMessage) {
if(!props.type && props.type != 'view'){
if(appData.value.type && appData.value.type == 'chatSimple' && !appData.value.modelId) {
messageTip("请选择AI模型");
@ -261,17 +297,30 @@
return;
}
}
// 检查是否需要设置额外参数
if (props.hasExtraFlowInputs) {
// 检查是否已设置
if (!props.conversationSettings || Object.keys(props.conversationSettings).length === 0) {
// 弹出设置弹窗,阻止发送
message.warning('请先设置对话参数');
emit('edit-settings');
return;
}
}
if (loading.value) return;
loading.value = true;
addChat(uuid.value, {
dateTime: new Date().toLocaleString(),
content: message,
content: userMessage,
images:uploadUrlList.value?uploadUrlList.value:[],
inversion: 'user',
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: null },
requestOptions: { prompt: userMessage, options: null },
eventType: 'message',
});
scrollToBottom();
@ -288,14 +337,15 @@
inversion: 'ai',
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
requestOptions: { prompt: userMessage, options: { ...options } },
referenceKnowledge: [],
eventType: 'message',
});
scrollToBottom();
//发送消息
sendMessage(message,options);
sendMessage(userMessage,options);
}
onUnmounted(() => {
@ -306,6 +356,10 @@
chatData.value.push({ ...data });
};
const updateChat = async (uuid, index, data) => {
let lastChatData = chatData.value[index];
if(lastChatData.showAvatar){
data.showAvatar = lastChatData.showAvatar;
}
chatData.value.splice(index, 1, data);
await scrollToBottom();
};
@ -343,6 +397,7 @@
loading: true,
conversationOptions: null,
requestOptions: null,
eventType: "message",
});
scrollToBottom();
};
@ -407,8 +462,11 @@
},{ isTransformResponse: false });
} finally {
handleStop();
localStorage.removeItem('chat_requestId_' + uuid.value);
}
//update-end---author:wangshuai---date:2025-06-03---for:【issues/8338】AI应用聊天回复stop无效仍会继续输出回复---
} else {
localStorage.removeItem('chat_requestId_' + uuid.value);
}
}
@ -426,6 +484,10 @@
topicId: topicId.value,
app: appData.value,
responseMode: 'streaming',
// 添加对话设置参数(调试模式也需要)
flowInputs: props.conversationSettings || {},
// 添加网络搜索参数
enableSearch: enableSearch.value
};
}else{
param = {
@ -434,7 +496,11 @@
images: uploadUrlList.value?uploadUrlList.value:[],
appId: appData.value.id,
responseMode: 'streaming',
conversationId: uuid.value === "1002"?'':uuid.value
conversationId: uuid.value === "1002"?'':uuid.value,
// 添加对话设置参数
flowInputs: props.conversationSettings || {},
// 添加网络搜索参数
enableSearch: enableSearch.value
};
if(headerTitle.value == '新建聊天'){
@ -502,7 +568,7 @@
*/
async function renderText(item,conversationId,text,options) {
let returnText = "";
if (item.event == 'MESSAGE') {
if (item.event == 'MESSAGE' || item.event == 'THINKING' || item.event == 'THINKING_END') {
let message = item.data.message;
let messageText = "";
//update-begin---author:wangshuai---date:2025-04-24---for:应该先判断是否包含card---
@ -518,20 +584,42 @@
if (item.requestId) {
requestId.value = item.requestId;
}
if(item.event == 'THINKING'){
isThinking.value = true;
}
if(item.event == 'MESSAGE' && isThinking.value){
text = item.data.message;
returnText = item.data.message;
//发送用户消息
addChat(uuid.value, {
dateTime: new Date().toLocaleString(),
content: item.data.message,
images:uploadUrlList.value?uploadUrlList.value:[],
inversion: 'ai',
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: null },
eventType: 'message',
showAvatar: 'no'
});
isThinking.value = false;
return { returnText, conversationId };
}
//更新聊天信息
updateChat(uuid.value, chatData.value.length - 1, {
dateTime: new Date().toLocaleString(),
content: messageText,
inversion: 'ai',
error: false,
loading: true,
loading: item.event == 'THINKING_END' ? false: true,
conversationOptions: { conversationId: conversationId, parentMessageId: topicId.value },
requestOptions: { prompt: message, options: { ...options } },
referenceKnowledge: knowList.value,
eventType: item.event.toLowerCase(),
});
}
if(item.event == 'INIT_REQUEST_ID'){
if (item.requestId) {
if (item.requestId && props.url != "/airag/app/debug") {
requestId.value = item.requestId;
localStorage.setItem('chat_requestId_' + uuid.value, JSON.stringify({ requestId: item.requestId, message: options.message }));
}
@ -547,6 +635,7 @@
//update-begin---author:wangshuai---date:2025-03-07---for:【QQYUN-11457】聊天调用流程执行失败了但是没提示---
if(item.data && !item.data.success){
updateChatFail(uuid, chatData.value.length - 1, item.data.message?item.data.message:'请求出错,请稍后重试!');
localStorage.removeItem('chat_requestId_' + uuid.value);
handleStop();
return "";
}
@ -555,10 +644,12 @@
conversationId = item.conversationId;
uuid.value = item.conversationId;
requestId.value = item.requestId;
localStorage.removeItem('chat_requestId_' + uuid.value);
handleStop();
}
if (item.event == 'ERROR') {
updateChatFail(uuid, chatData.value.length - 1, item.data.message?item.data.message:'请求出错,请稍后重试!');
localStorage.removeItem('chat_requestId_' + uuid.value);
handleStop();
return "";
}
@ -600,6 +691,7 @@
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
referenceKnowledge: knowList.value,
eventType: 'message',
});
}
}
@ -609,8 +701,8 @@
if(!item.data || item.data.type !== 'end'){
if(item.data.type === 'knowledge'){
const id = item.data.id;
const data = item.data.outputs[id + ".data"]
knowList.value.push(data)
const data = item.data.outputs[id + ".documents"]
knowList.value = data
//更新聊天信息
updateChatSome(uuid.value, chatData.value.length - 1, {referenceKnowledge: knowList.value})
}
@ -840,6 +932,18 @@
}
}
}
//update-begin---author:wangshuai---date:2025-11-05---for: 如果是断线重连并且文本为空,需要移出前面两条会话---
if(!text && isReConnect && chatData.value.length >1){
//如果是断线重连的情况下流结果为空时移除占位的AI消息避免空结果也新增聊天记录
const lastMsg = chatData.value[chatData.value.length - 1];
if (lastMsg && lastMsg.inversion === 'ai' && lastMsg.content === '请稍后') {
chatData.value.splice(chatData.value.length - 1, 1);
chatData.value.splice(chatData.value.length - 1, 1);
}
//update-end---author:wangshuai---date:2025-11-05---for: 如果是断线重连并且文本为空,需要移出前面两条会话---
localStorage.removeItem('chat_requestId_' + uuid.value);
loading.value = false;
}
}
/**
@ -858,9 +962,11 @@
timeout: 5 * 60 * 1000
}, { isTransformResponse: false }).catch(async (err)=>{
loading.value = false;
localStorage.removeItem('chat_requestId_' + uuid.value);
});
if(result && message){
loading.value = true;
isReConnect.value = true;
//发送用户消息
addChat(uuid.value, {
dateTime: new Date().toLocaleString(),
@ -870,6 +976,7 @@
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: null },
eventType: 'message',
});
let options: any = {};
const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions;
@ -886,6 +993,7 @@
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
referenceKnowledge: [],
eventType: 'message',
});
options.message = message;
scrollToBottom();
@ -893,7 +1001,11 @@
await renderChatByResult(result,options);
} else {
loading.value = false;
localStorage.removeItem('chat_requestId_' + uuid.value);
isReConnect.value = false;
}
} else {
isReConnect.value = false;
}
}
@ -924,12 +1036,52 @@
try {
if (val) {
appData.value = val;
// 检查模型是否支持网络搜索
checkModelProvider();
}
} catch (e) {}
},
{ deep: true, immediate: true }
);
// 编辑对话设置
function handleEditSettings() {
emit('edit-settings');
}
// 切换网络搜索
function toggleWebSearch() {
enableSearch.value = !enableSearch.value;
if (enableSearch.value) {
message.success("已开启联网搜索");
} else {
message.info("已关闭联网搜索");
}
}
// 检查模型是否支持网络搜索从appData.metadata.modelInfo中获取
function checkModelProvider() {
if (appData.value && appData.value.metadata) {
try {
const metadata = typeof appData.value.metadata === 'string'
? JSON.parse(appData.value.metadata)
: appData.value.metadata;
if (metadata && metadata.modelInfo) {
modelProvider.value = metadata.modelInfo.provider || '';
// 只有千问模型支持网络搜索
showWebSearch.value = modelProvider.value === 'QWEN';
} else {
showWebSearch.value = false;
}
} catch (e) {
console.error('解析模型信息失败', e);
showWebSearch.value = false;
}
} else {
showWebSearch.value = false;
}
}
//监听历史信息
watch(
() => props.historyData,
@ -945,7 +1097,9 @@
chatData.value = [];
headerTitle.value = props.chatTitle;
}
if(props.prologue && props.chatTitle){
//update-begin---author:wangshuai---date:2025-11-18---for:【QQYUN-14049】【AI】没有开场白就不展示预设问题了---
if((props.prologue || props.presetQuestion) && props.chatTitle){
//update-end---author:wangshuai---date:2025-11-18---for:【QQYUN-14049】【AI】没有开场白就不展示预设问题了---
topChat(props.prologue)
}
//ai回复重连
@ -962,6 +1116,8 @@
scrollToBottom();
uploadUrlList.value = [];
fileInfoList.value = [];
// 检查模型是否支持网络搜索
checkModelProvider();
});
</script>
@ -1048,6 +1204,16 @@
display: flex;
padding: 8px;
align-items: center;
&.enabled {
color: @primary-color;
}
}
.webSearchBtn {
&.enabled {
:deep(.anticon) {
color: #52c41a !important;
}
}
}
.stopBtn {
width: 32px;
@ -1070,12 +1236,46 @@
font-weight: 400;
padding-bottom: 8px;
margin-left: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
height: 30px;
.title-content{
display: flex;
align-items: center;
gap: 4px;
overflow: hidden;
> span{
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.header-actions{
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.edit-btn{
padding: 2px 4px;
color: #999;
flex-shrink: 0;
height: 24px;
&:hover{
color: @primary-color;
}
:deep(.anticon){
font-size: 16px;
}
}
.header-advertisint{
display:flex;
margin-right: 20px;

View File

@ -1,11 +1,11 @@
<template>
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']" v-if="getText || (props.presetQuestion && props.presetQuestion.length>0)">
<div class="avatar">
<div class="avatar" v-if="showAvatar !== 'no'">
<img v-if="inversion === 'user'" :src="avatar()" />
<img v-else :src="getAiImg()" />
</div>
<div class="content">
<p class="date">
<p class="date" v-if="showAvatar !== 'no'">
<span v-if="inversion === 'ai'" style="margin-right: 10px">{{appData.name || 'AI助手'}}</span>
<span>{{ dateTime }}</span>
</p>
@ -30,7 +30,14 @@
</a-col>
</a-row>
</div>
<div class="msgArea" v-if="!isCard">
<div class="thinkArea" style="margin-bottom: 10px" v-if="!isCard && (eventType === 'thinking' || eventType === 'thinking_end')">
<a-collapse v-model:activeKey="activeKey" ghost>
<a-collapse-panel :key="uuid" :header="loading?'正在思考中':'思考结束'">
<ThinkText :text="text" :inversion="inversion" :error="error" :loading="loading"></ThinkText>
</a-collapse-panel>
</a-collapse>
</div>
<div class="msgArea" v-else-if="!isCard" :class="showAvatar == 'no' ? 'hidden-avatar' : ''">
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading" :referenceKnowledge="referenceKnowledge"></chatText>
</div>
<div v-if="presetQuestion" v-for="item in presetQuestion" class="question" @click="presetQuestionClick(item.descr)">
@ -42,15 +49,20 @@
<script setup lang="ts">
import chatText from './chatText.vue';
import ThinkText from './ThinkText.vue';
import defaultAvatar from "@/assets/images/ai/avatar.jpg";
import { useUserStore } from '/@/store/modules/user';
import defaultImg from '../img/ailogo.png';
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading','appData','presetQuestion','images','retrievalText', 'referenceKnowledge']);
import { ref } from 'vue';
import { buildUUID } from '/@/utils/uuid';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { createImgPreview } from "@/components/Preview";
import { computed } from "vue";
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading','appData','presetQuestion','images','retrievalText', 'referenceKnowledge', 'eventType', 'showAvatar']);
const uuid = ref<any>(buildUUID());
const activeKey = ref<any>(uuid.value);
const getText = computed(()=>{
let text = props.text || props.retrievalText;
if(text){
@ -147,12 +159,26 @@
}
.msgArea {
flex-direction: row-reverse;
margin-bottom: 6px;
}
.thinkArea{
margin: 0;
padding: 5px 0 5px 22px;
position: relative;
}
.date {
text-align: right;
}
}
}
:deep(.ant-collapse-header){
padding: 0 !important;
}
.hidden-avatar{
left: 44px;
position: relative;
top: -18px;
}
.avatar {
flex: none;
margin-right: 10px;

View File

@ -4,13 +4,16 @@
<div class="markdown-body" :class="{ 'markdown-body-generate': loading }" :style="{color:error?'#FF4444 !important':''}" v-html="text" />
<template v-if="showRefKnow">
<a-divider orientation="left">引用</a-divider>
<template v-for="(item, idx) of referenceKnowledge" :key="idx">
<a-tooltip :title="item.substring(0, 800)">
<a-tag >
<a-space>
<img :src="knowledgePng" width="16" height="16"/>
<template v-for="(item, idx) in referenceKnowledge" :key="idx">
<a-tooltip :title="item.content?.substring(0, 800)">
<a-tag style="min-width: 80px;background: #F7F8FA;padding-inline: 0 7px">
<a-space style="min-height: 30px;padding-left: 4px;padding-right: 4px;background-color: #F0F1F6;color: #788194">
<div>{{ 'chunk-' + item.chunk}}</div>
</a-space>
<a-space style="min-height: 30px;padding-left: 4px;">
<img :src="knowledgePng" width="14" height="14" style="position: relative; top: -2px"/>
<div style="max-width: 240px; overflow: hidden;white-space: nowrap;text-overflow: ellipsis;">
{{ item }}
{{ item.docName }}
</div>
</a-space>
</a-tag>

View File

@ -0,0 +1,382 @@
<template>
<a-modal
v-model:open="visible"
title="对话设置"
:width="600"
:maskClosable="false"
:keyboard="false"
@ok="handleOk"
okText="开始对话"
:cancelButtonProps="{ style: { display: 'none' } }"
:okButtonProps="{ disabled: !canSubmit }"
>
<div class="settings-content">
<a-form :model="formData" layout="vertical">
<a-form-item
v-for="input in flowInputs"
:key="input.field"
:label="input.name"
:required="input.required"
>
<!-- 文本类型 - 使用单行输入 -->
<a-input
v-if="input.type === 'string'"
v-model:value="formData[input.field]"
:placeholder="`请输入${input.name}`"
:maxlength="input.maxLength"
show-count
/>
<!-- 数字类型 -->
<a-input-number
v-else-if="input.type === 'number'"
v-model:value="formData[input.field]"
:placeholder="`请输入${input.name}`"
style="width: 100%"
:min="input.min"
:max="input.max"
/>
<!-- 图片类型 - 使用类似chat.vue的上传样式固定支持3张图片 -->
<div v-else-if="input.type === 'picture'" class="image-upload-container">
<div class="image-list-wrapper">
<!-- 已上传的图片 -->
<div v-for="(img, idx) in imageFileList[input.field]" :key="idx" class="image-preview-item">
<img :src="getImageUrl(img)" @click="handlePreview(img)" />
<div class="image-remove-icon" @click="handleRemove(idx, input.field)">
<Icon icon="ant-design:close-outlined" :size="12" />
</div>
</div>
<!-- 上传按钮 -->
<a-upload
v-if="(imageFileList[input.field]?.length || 0) < 3"
accept=".jpg,.jpeg,.png"
name="file"
:showUploadList="false"
:headers="headers"
:beforeUpload="(file) => beforeUpload(file, input.field)"
@change="(info) => handleChange(info, input.field)"
:multiple="true"
:action="uploadUrl"
:max-count="3"
>
<div class="upload-trigger">
<Icon icon="ant-design:plus-outlined" :size="20" />
<div class="upload-text">上传图片</div>
</div>
</a-upload>
</div>
</div>
<!-- 字符串数组类型如history -->
<a-select
v-else-if="input.type === 'string[]'"
v-model:value="formData[input.field]"
mode="tags"
:placeholder="`请输入${input.name}`"
style="width: 100%"
/>
<!-- 其他类型使用文本输入 -->
<a-input
v-else
v-model:value="formData[input.field]"
:placeholder="`请输入${input.name}`"
/>
</a-form-item>
</a-form>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { message } from 'ant-design-vue';
import { Icon } from '/@/components/Icon';
import { getFileAccessHttpUrl, getHeaders } from '@/utils/common/compUtils';
import { useGlobSetting } from '@/hooks/setting';
const props = defineProps<{
flowInputs: any[];
conversationId: string;
existingSettings?: Record<string, any>;
}>();
const emit = defineEmits<{
(e: 'ok', data: Record<string, any>): void;
(e: 'cancel'): void;
}>();
const visible = ref(false);
const formData = ref<Record<string, any>>({});
const imageFileList = ref<Record<string, any[]>>({});
const globSetting = useGlobSetting();
const baseUploadUrl = globSetting.uploadUrl;
const uploadUrl = `${baseUploadUrl}/airag/chat/upload`;
const headers = getHeaders();
// 过滤掉固定参数history, content, images
const flowInputs = computed(() => {
const fixedParams = ['history', 'content', 'images'];
return props.flowInputs?.filter((input) => !fixedParams.includes(input.field)) || [];
});
// 检查是否可以提交(必填项都已填写)
const canSubmit = computed(() => {
if (flowInputs.value.length === 0) return true;
return flowInputs.value.every((input) => {
if (!input.required) return true;
const value = formData.value[input.field];
if (input.type === 'picture') {
return imageFileList.value[input.field] && imageFileList.value[input.field].length > 0;
}
return value !== undefined && value !== null && value !== '';
});
});
// 上传前处理
const beforeUpload = (file: any, field: string) => {
const isImage = file.type?.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件!');
return false;
}
const currentCount = imageFileList.value[field]?.length || 0;
if (currentCount >= 3) {
message.warning('最多只能上传3张图片');
return false;
}
return true;
};
// 上传状态变化处理
const handleChange = (info: any, field: string) => {
const { file } = info;
if (file.status === 'error' || (file.response && file.response.code === 500)) {
message.error(file.response?.message || `${file.name} 上传失败`);
return;
}
if (file.status === 'done' && file.response) {
const imageUrl = file.response.message;
if (!imageFileList.value[field]) {
imageFileList.value[field] = [];
}
// 检查是否已达到上限
if (imageFileList.value[field].length >= 3) {
message.warning('最多只能上传3张图片');
return;
}
imageFileList.value[field].push(imageUrl);
// 图片类型始终作为数组存储,触发响应式更新
imageFileList.value = { ...imageFileList.value };
formData.value[field] = [...imageFileList.value[field]];
console.log(`[图片上传] 当前图片数量: ${imageFileList.value[field].length}`, imageFileList.value[field]);
}
};
// 获取图片URL
const getImageUrl = (img: any) => {
if (typeof img === 'string') {
return getFileAccessHttpUrl(img);
}
return getFileAccessHttpUrl(img.url || img);
};
// 图片预览
const handlePreview = (img: any) => {
const url = typeof img === 'string' ? img : (img.url || img);
const imageUrl = getFileAccessHttpUrl(url);
// 可以使用 ant-design-vue 的 Image 预览功能
window.open(imageUrl, '_blank');
};
// 移除图片
const handleRemove = (index: number, field: string) => {
if (imageFileList.value[field]) {
imageFileList.value[field].splice(index, 1);
// 触发响应式更新
imageFileList.value = { ...imageFileList.value };
// 图片类型始终作为数组存储,删除后更新
formData.value[field] = imageFileList.value[field].length > 0
? [...imageFileList.value[field]]
: [];
console.log(`[图片删除] 当前图片数量: ${imageFileList.value[field].length}`, imageFileList.value[field]);
}
};
// 打开弹窗
const open = () => {
visible.value = true;
// 初始化表单数据
formData.value = {};
imageFileList.value = {};
// 如果有已存在的设置,填充表单
if (props.existingSettings && Object.keys(props.existingSettings).length > 0) {
Object.keys(props.existingSettings).forEach((key) => {
const input = props.flowInputs.find((i) => i.field === key);
if (input) {
if (input.type === 'picture') {
// 确保是数组格式
const urls = Array.isArray(props.existingSettings![key])
? props.existingSettings![key]
: (props.existingSettings![key] ? [props.existingSettings![key]] : []);
imageFileList.value[key] = urls.filter(url => url); // 过滤空值
formData.value[key] = [...imageFileList.value[key]]; // 始终作为数组
} else {
formData.value[key] = props.existingSettings![key];
}
}
});
}
};
// 确定
const handleOk = async () => {
if (!canSubmit.value) {
message.warning('请填写所有必填项');
return;
}
// 构建最终数据
const result: Record<string, any> = {};
flowInputs.value.forEach((input) => {
const value = formData.value[input.field];
if (value !== undefined && value !== null && value !== '') {
result[input.field] = value;
}
});
// 先保存,再触发事件
// 直接通过emit返回设置数据不需要单独保存
// 设置会在发送消息时自动保存到后端
emit('ok', result);
visible.value = false;
};
// 暴露方法
defineExpose({
open,
});
</script>
<style scoped lang="less">
.settings-content {
max-height: 60vh;
overflow-y: auto;
padding: 10px 5px;
:deep(.ant-form-item) {
margin-bottom: 16px;
}
:deep(.ant-form-item-label) {
padding: 0 5px;
}
:deep(.ant-input),
:deep(.ant-input-number),
:deep(.ant-select) {
margin: 0 5px;
width: calc(100% - 10px) !important;
}
:deep(.ant-upload) {
margin: 0 5px;
}
}
.image-upload-container {
margin: 0 5px;
.image-list-wrapper {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.image-preview-item {
position: relative;
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #d9d9d9;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
}
.image-remove-icon {
position: absolute;
top: 4px;
right: 4px;
background-color: rgba(0, 0, 0, 0.6);
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s;
&:hover {
background-color: rgba(0, 0, 0, 0.8);
}
}
&:hover .image-remove-icon {
opacity: 1;
}
}
.upload-trigger {
width: 80px;
height: 80px;
border: 1px dashed #d9d9d9;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
background-color: #fafafa;
flex-shrink: 0;
&:hover {
border-color: #1890ff;
color: #1890ff;
}
.upload-text {
margin-top: 4px;
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
}
&:hover .upload-text {
color: #1890ff;
}
}
}
</style>

View File

@ -59,10 +59,10 @@
import { useModal, useModalInner } from '@/components/Modal';
import { Pagination } from 'ant-design-vue';
import { list } from '@/views/super/airag/aiknowledge/AiKnowledgeBase.api';
// import {pageApi} from "@/views/super/airag/aiflow/pages/api";
import { defHttp } from "@/utils/http/axios";
import knowledge from '/@/views/super/airag/aiknowledge/icon/knowledge.png';
import { cloneDeep } from 'lodash-es';
// import {pageApi} from "@/views/super/airag/aiflow/pages/api";
import { defHttp } from "@/utils/http/axios";
import { getFileAccessHttpUrl } from "@/utils/common/compUtils";
import defaultFlowImg from "@/assets/images/ai/aiflow.png";
@ -154,7 +154,7 @@
async function getAiFlowList(params?: any) {
return defHttp.get({url: '/airag/flow/list', params});
}
/**
* 分页改变事件
* @param page

View File

@ -0,0 +1,348 @@
<template>
<div class="p-2">
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="600px" :title="title" @ok="handleOk" @cancel="handleCancel">
<div class="flex header">
<a-input
@keyup.enter="loadMcpData"
class="header-search"
size="small"
v-model:value="searchText"
placeholder="请输入MCP名称回车搜索"
></a-input>
</div>
<a-row :span="24">
<a-col :span="12" v-for="item in mcpOption" :key="item.id" @click="handleSelect(item)">
<a-card :body-style="{padding: '10px 12px'}" hoverable :class="['mcp-card', { 'is-active': item.checked }]">
<div class="mcp-card-header">
<div class="mcp-card-left">
<img class="mcp-card-icon" :src="getIcon(item.icon)" />
<div class="mcp-card-info">
<div class="mcp-card-name" :title="item.name">{{ item.name }}</div>
<div class="mcp-card-meta">
<div class="pill type-pill" :title="'类型: '+(item.category === 'plugin' ? '插件' : 'MCP')">
<Icon :icon="getCategoryIcon(item.category)" class="pill-icon" />
<span class="pill-text">{{ item.category === 'plugin' ? '插件' : 'MCP' }}</span>
</div>
<div class="pill tool-pill" :title="getToolCount(item.metadata)+' 个工具'">
<Icon icon="ant-design:tool-outlined" class="pill-icon" />
<span class="pill-text">{{ getToolCount(item.metadata) }}</span>
</div>
</div>
</div>
</div>
<a-checkbox v-model:checked="item.checked" @click.stop class="mcp-card-checker" @change="(e)=>handleChange(e,item)"> </a-checkbox>
</div>
</a-card>
</a-col>
</a-row>
<div v-if="pluginIds.length > 0" class="use-select">
已选择 {{ pluginIds.length }} 个MCP
<span style="margin-left: 8px; color: #3d79fb; cursor: pointer" @click="handleClearClick">清空</span>
</div>
<Pagination
v-if="mcpOption.length > 0"
:current="pageNo"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:total="total"
:showQuickJumper="true"
:showSizeChanger="true"
@change="handlePageChange"
class="list-footer"
size="small"
/>
</BasicModal>
</div>
</template>
<script lang="ts">
import { ref } from 'vue';
import BasicModal from '@/components/Modal/src/BasicModal.vue';
import { useModalInner } from '@/components/Modal';
import { Pagination } from 'ant-design-vue';
import { list as mcpList } from '@/views/super/airag/aimcp/AiragMcp.api';
import { getFileAccessHttpUrl } from '@/utils/common/compUtils';
import defaultLogo from '@/views/super/airag/aimcp/imgs/mcpLogo.png';
import { Icon } from '/@/components/Icon';
export default {
name: 'AiAppAddMcpModal',
components: {
Pagination,
BasicModal,
Icon,
},
emits: ['success', 'register'],
setup(props, { emit }) {
const title = ref<string>('添加关联MCP');
const mcpOption = ref<any>([]);
const pluginIds = ref<any>([]); // 仅存放id
const pluginDataList = ref<any>([]); // 选中对象
const pageNo = ref<number>(1);
const pageSize = ref<number>(10);
const total = ref<number>(0);
const searchText = ref<string>('');
const pageSizeOptions = ref<any>(['10', '20', '30']);
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
pluginIds.value = data.pluginIds ? [...data.pluginIds] : [];
pluginDataList.value = data.pluginDataList ? [...data.pluginDataList] : [];
setModalProps({ minHeight: 500, bodyStyle: { padding: '10px' } });
loadMcpData();
});
function getIcon(icon){
return icon ? getFileAccessHttpUrl(icon) : defaultLogo;
}
async function handleOk() {
// 拼接插件结构使用item的category字段
const plugins = pluginDataList.value.map((item:any)=>({
pluginId: item.id,
pluginName: item.name,
category: item.category || 'mcp'
}));
emit('success', pluginIds.value, pluginDataList.value, plugins);
handleCancel();
}
function handleCancel() {
closeModal();
}
function handleSelect(item:any){
const id = item.id;
const target = mcpOption.value.find((it:any)=> it.id === id);
if(target){
target.checked = !target.checked;
}
if(!pluginIds.value || pluginIds.value.length===0){
pluginIds.value.push(id);
pluginDataList.value.push(item);
return;
}
const findIndex = pluginIds.value.findIndex((val:any)=> val === id);
if(findIndex === -1){
pluginIds.value.push(id);
pluginDataList.value.push(item);
}else{
pluginIds.value.splice(findIndex,1);
pluginDataList.value.splice(findIndex,1);
}
}
function loadMcpData(){
const params = { pageNo: pageNo.value, pageSize: pageSize.value, status: 'enable', synced: 1, name: searchText.value };
mcpList(params).then((res:any)=>{
if (res.records) {
const records = res.records || [];
if(pluginIds.value.length>0){
for(const rec of records){
if(pluginIds.value.includes(rec.id)){
rec.checked = true;
}
}
}
mcpOption.value = records;
total.value = res.total;
}else{
mcpOption.value = [];
total.value = 0;
}
});
}
function handlePageChange(page:number, current:number){
pageNo.value = page;
pageSize.value = current;
loadMcpData();
}
function handleClearClick(){
pluginIds.value = [];
pluginDataList.value = [];
mcpOption.value.forEach((item:any)=> item.checked = false);
}
function handleChange(e:any, item:any){
if(e.target.checked){
pluginIds.value.push(item.id);
pluginDataList.value.push(item);
}else{
const findIndex = pluginIds.value.findIndex((val:any)=> val === item.id);
if(findIndex>-1){
pluginIds.value.splice(findIndex,1);
pluginDataList.value.splice(findIndex,1);
}
}
}
// 工具数量:从 metadata 中读取 tool_count
function getToolCount(metadata: any): number {
if (!metadata) return 0;
let metaObj: any = metadata;
if (typeof metadata === 'string') {
try {
metaObj = JSON.parse(metadata);
} catch (e) {
return 0;
}
}
const count = metaObj.tool_count || metaObj.toolCount || 0;
return typeof count === 'number' ? count : parseInt(count, 10) || 0;
}
// 类型图标映射
function getTypeIcon(type?: string) {
switch (type) {
case 'sse':
return 'ant-design:thunderbolt-outlined';
case 'stdio':
return 'ant-design:code-outlined';
default:
return 'ant-design:appstore-outlined';
}
}
// category图标映射
function getCategoryIcon(category?: string) {
if (category === 'plugin') {
return 'ant-design:api-outlined';
}
return 'ant-design:tool-twotone';
}
return {
registerModal,
title,
handleOk,
handleCancel,
mcpOption,
pluginIds,
pluginDataList,
pageNo,
pageSize,
pageSizeOptions,
total,
handlePageChange,
searchText,
loadMcpData,
handleClearClick,
handleChange,
handleSelect,
getIcon,
getToolCount,
getTypeIcon,
getCategoryIcon,
};
},
};
</script>
<style scoped lang="less">
.header {
color: #646a73;
width: 100%;
justify-content: space-between;
margin-bottom: 10px;
.header-search {
width: 200px;
}
}
.mcp-card {
margin-bottom: 10px;
margin-right: 10px;
border: 1px solid #e5e6eb;
border-radius: 8px;
background: #fff;
transition: box-shadow 0.25s, border-color 0.25s;
cursor: pointer;
&.is-active {
border-color: #3370ff;
box-shadow: 0 4px 10px rgba(0,0,0,0.08);
}
&:hover {
box-shadow: 0 4px 10px rgba(0,0,0,0.08);
}
}
.mcp-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
width: 100%;
}
.mcp-card-left {
display: flex;
align-items: flex-start;
min-width: 0;
flex: 1;
}
.mcp-card-icon {
width: 40px;
height: 40px;
border-radius: 8px;
object-fit: cover;
background: #f5f6f7;
flex-shrink: 0;
}
.mcp-card-info {
margin-left: 8px;
flex: 1;
min-width: 0;
}
.mcp-card-name {
font-size: 14px;
font-weight: 600;
color: #1d2129;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 6px;
}
.mcp-card-meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.mcp-card-checker {
margin-left: 12px;
flex-shrink: 0;
}
.pill {
display: inline-flex;
align-items: center;
padding: 2px 8px 2px 6px;
border-radius: 12px;
font-size: 11px;
line-height: 16px;
font-weight: 500;
backdrop-filter: saturate(180%) blur(4px);
box-shadow: 0 0 0 1px rgba(0,0,0,0.05);
.pill-icon {
margin-right: 3px;
font-size: 12px;
}
}
.type-pill {
background: linear-gradient(135deg,#e6f4ff,#f0f9ff);
color:#0958d9;
}
.tool-pill {
background: linear-gradient(135deg,#f5f6f7,#f0f1f2);
color:#555;
}
.use-select {
color: #646a73;
position: absolute;
bottom: 0;
left: 20px;
}
.list-footer {
position: absolute;
bottom: 0;
right: 10px;
}
</style>

View File

@ -116,7 +116,7 @@
async function generatedPrompt() {
content.value = '';
loading.value = true;
let readableStream = await promptGenerate({ prompt: prompt.value }).catch(() => {
let readableStream = await promptGenerate({ prompt: encodeURIComponent(prompt.value) }).catch(() => {
loading.value = false;
});
const reader = readableStream.getReader();

View File

@ -80,7 +80,7 @@
<span>提示词</span>
<a-button v-if="!isRelease" size="middle" @click="generatedPrompt" ghost>
<span style="align-items: center;display:flex">
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"></path></svg>
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"/></svg>
<span style="margin-left: 4px">生成</span>
</span>
</a-button>
@ -147,7 +147,7 @@
<div class="quick-command">
<div style="display: flex;align-items: center">
<Icon v-if="item.icon" :icon="item.icon" size="20"></Icon>
<svg v-else width="14px" height="14px" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"></path></svg>
<svg v-else width="14px" height="14px" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"/></svg>
<div style="max-width: 400px;margin-left: 4px" class="ellipsis">{{item.name}}</div>
</div>
<div v-if="!isRelease" style="align-items: center" class="quick-command-icon">
@ -185,6 +185,7 @@
placeholder="请选择AI模型"
dict-code="airag_model where model_type = 'LLM' and activate_flag = 1,name,id"
style="width: 100%;"
@change="handleModelIdChange"
></JDictSelectTag>
</a-form-item>
</div>
@ -231,6 +232,43 @@
</a-form-item>
</div>
</a-col>
<!-- 插件关联区块 -->
<a-col :span="24" v-if="formState.type==='chatSimple'" class="mt-10">
<div class="prologue-chunk">
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol">
<template #label>
<div style="display: flex; justify-content: space-between; width: 100%;margin-left: 2px;">
<div class="item-title">关联MCP&插件</div>
<div v-if="!isRelease">
<span @click="handleAddMcpClick" class="knowledge-txt">
<Icon icon="ant-design:plus-outlined" size="13" style="margin-right: 2px"></Icon>添加
</span>
</div>
</div>
</template>
<a-row :span="24">
<a-col :span="12" v-for="item in pluginDataList" :key="item.id" v-if="pluginDataList && pluginDataList.length>0">
<a-card hoverable class="knowledge-card" :body-style="{ width: '100%' }">
<div style="display: flex; width: 100%; justify-content: space-between">
<div>
<img class="knowledge-img" src="/@/views/super/airag/aimcp/imgs/mcpLogo.png" />
<span class="knowledge-name">{{ item.name }}</span>
</div>
<Icon v-if="!isRelease" @click="handleDeleteMcp(item.id)" icon="ant-design:close-outlined" size="20" class="knowledge-icon"></Icon>
</div>
</a-card>
</a-col>
<div v-else class="data-empty-text">
<div style="margin-bottom: 8px;">添加插件后智能体可调用外部工具能力丰富对话</div>
<div class="mcp-warning-tip">
<Icon icon="ant-design:exclamation-circle-outlined" style="margin-right: 4px;" />
<span>注意部分大模型暂不支持工具调用功能请确认所选模型兼容性</span>
</div>
</div>
</a-row>
</a-form-item>
</div>
</a-col>
<a-col :span="24" class="mt-10">
<div class="prologue-chunk">
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol" v-bind="validateInfos.msgNum">
@ -258,7 +296,18 @@
</a-form>
</a-col>
<a-col :span="14" class="setting-right">
<chat :uuid="uuid" :prologue="prologue" :appId="appId" :formState="formState" url="/airag/app/debug" :presetQuestion="presetQuestion" :quickCommandData="quickCommandList"></chat>
<chat
:uuid="uuid"
:prologue="prologue"
:appId="appId"
:formState="formState"
url="/airag/app/debug"
:presetQuestion="presetQuestion"
:quickCommandData="quickCommandList"
:hasExtraFlowInputs="hasExtraFlowInputs"
:conversationSettings="conversationSettings"
@edit-settings="handleEditSettings"
></chat>
</a-col>
</a-row>
</div>
@ -266,6 +315,8 @@
<!-- Ai知识库选择弹窗 -->
<AiAppAddKnowledgeModal @register="registerKnowledgeModal" @success="handleSuccess"></AiAppAddKnowledgeModal>
<!-- 插件选择弹窗 -->
<AiAppAddMcpModal @register="registerMcpModal" @success="handleMcpSuccess"></AiAppAddMcpModal>
<!-- Ai添加流程弹窗 -->
<AiAppAddFlowModal @register="registerFlowModal" @success="handleAddFlowSuccess"></AiAppAddFlowModal>
<!-- Ai配置弹窗 -->
@ -276,24 +327,35 @@
<AiAppGeneratedPromptModal @register="registerAiAppPromptModal" @ok="handleAiAppPromptOk"></AiAppGeneratedPromptModal>
<!-- Ai快捷指令 -->
<AiAppQuickCommandModal @register="registerAiAppCommandModal" @ok="handleAiAppCommandOk" @update-ok="handleAiAppCommandUpdateOk"></AiAppQuickCommandModal>
<!-- 对话设置弹窗 -->
<ConversationSettingsModal
ref="settingsModalRef"
:flowInputs="flowInputs"
conversationId="debug"
:existingSettings="conversationSettings"
@ok="handleSettingsOk"
/>
</div>
</template>
<script lang="ts">
import { ref, reactive, nextTick, computed } from 'vue';
import { ref, reactive, nextTick, computed, watch } from 'vue';
import BasicModal from '@/components/Modal/src/BasicModal.vue';
import { useModal, useModalInner } from '@/components/Modal';
import { Form, TimePicker } from 'ant-design-vue';
import { initDictOptions } from '@/utils/dict';
import {queryKnowledgeBathById, saveApp, queryById, queryFlowById} from '../AiApp.api';
import { defHttp } from '@/utils/http/axios';
import JDictSelectTag from '@/components/Form/src/jeecg/components/JDictSelectTag.vue';
import AiAppAddKnowledgeModal from './AiAppAddKnowledgeModal.vue';
import AiAppAddMcpModal from './AiAppAddMcpModal.vue';
import AiAppParamsSettingModal from './AiAppParamsSettingModal.vue';
import AiAppGeneratedPromptModal from './AiAppGeneratedPromptModal.vue';
import AiAppQuickCommandModal from './AiAppQuickCommandModal.vue';
import AiAppAddFlowModal from './AiAppAddFlowModal.vue';
import AiAppModal from './AiAppModal.vue';
import chat from '../chat/chat.vue';
import ConversationSettingsModal from '../chat/components/ConversationSettingsModal.vue';
import knowledge from '/@/views/super/airag/aiknowledge/icon/knowledge.png';
import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
@ -316,10 +378,12 @@
JDictSelectTag,
BasicModal,
AiAppAddKnowledgeModal,
AiAppAddMcpModal,
AiAppParamsSettingModal,
AiAppAddFlowModal,
AiAppModal,
chat,
ConversationSettingsModal,
AiAppGeneratedPromptModal,
AiAppQuickCommandModal,
},
@ -356,15 +420,20 @@
flowId:[{ required: true, message: '请选择AI流程!' }]
});
//注册form
const formRef = ref();
const useForm = Form.useForm;
const { resetFields, validate, validateInfos, validateField } = useForm(formState, validatorRules, { immediate: false });
const { resetFields, validate, validateInfos } = useForm(formState, validatorRules, { immediate: false });
const labelCol = ref<any>({ span: 24 });
const wrapperCol = ref<any>({ span: 24 });
//关联知识库的id
const knowledgeIds = ref<any>('');
//知识库集合
const knowledgeDataList = ref<any>([]);
// 插件id集合只存id
const pluginIds = ref<any>([]);
// 插件对象集合包含name等
const pluginDataList = ref<any>([]);
// 插件结构(保存时写入 formState.plugins
const plugins = ref<any>([]);
//开场白的数据
const prologue = ref<any>('');
//应用id
@ -383,6 +452,12 @@
const multiSessionChecked = ref<boolean>(true);
// 是否已发布
const isRelease = ref<boolean>(false);
//对话设置弹窗ref
const settingsModalRef = ref();
//工作流入参列表
const flowInputs = ref<any[]>([]);
//对话设置(用于调试模式)
const conversationSettings = ref<Record<string, any>>({});
//注册modal
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
appId.value = data.id;
@ -416,6 +491,7 @@
//注册modal
const [registerKnowledgeModal, { openModal }] = useModal();
const [registerMcpModal, { openModal: openMcpModal }] = useModal();
const [registerFlowModal, { openModal: registerFlowOpen }] = useModal();
const [registerParamsSettingModal, { openModal: paramsSettingOpen }] = useModal();
const [registerAiAppModal, { openModal: aiAppModalOpen }] = useModal();
@ -479,6 +555,16 @@
});
}
/**
* 添加关联MCP
*/
function handleAddMcpClick(){
openMcpModal(true,{
pluginIds: pluginIds.value,
pluginDataList: pluginDataList.value
})
}
/**
* 选中回调事件
* @param knowledgeId
@ -492,6 +578,19 @@
formState.knowledgeIds = knowledgeIds.value;
}
/**
* MCP选中成功回调
* @param ids
* @param dataList
* @param pluginStruct
*/
function handleMcpSuccess(ids, dataList, pluginStruct){
pluginIds.value = cloneDeep(ids);
pluginDataList.value = cloneDeep(dataList);
plugins.value = cloneDeep(pluginStruct);
formState.plugins = JSON.stringify(plugins.value);
}
/**
* 删除知识库
*/
@ -506,6 +605,19 @@
}
}
/**
* 删除MCP
*/
function handleDeleteMcp(id){
const findIndex = pluginIds.value.findIndex((item:any)=> item === id);
if(findIndex>-1){
pluginIds.value.splice(findIndex,1);
pluginDataList.value.splice(findIndex,1);
plugins.value = pluginDataList.value.map((it:any)=> ({ pluginId: it.id, pluginName: it.name, category: 'mcp'}));
formState.plugins = JSON.stringify(plugins.value);
}
}
/**
* 根据知识库id查询知识库内容
* @param ids
@ -764,6 +876,9 @@
function clearParam() {
knowledgeIds.value = '';
knowledgeDataList.value = [];
pluginIds.value = [];
pluginDataList.value = [];
plugins.value = [];
prologue.value = '';
flowId.value = '';
flowData.value = null;
@ -790,7 +905,7 @@
if(metadata.value?.multiSession){
multiSessionChecked.value = metadata.value.multiSession === '1';
}else{
multiSessionChecked.value = "1";
multiSessionChecked.value = true;
}
}
if(data.presetQuestion){
@ -806,6 +921,18 @@
quickCommandList.value = parse;
//update-end---author:wangshuai---date:2025-04-08---for:【QQYUN-11939】ai应用 快捷指令 修改保存以后,再次打开还是原来的---
}
if(data.plugins){
try {
const parsePlugins = JSON.parse(data.plugins);
pluginIds.value = parsePlugins.map((p:any)=> p.pluginId);
pluginDataList.value = parsePlugins.map((p:any)=> ({ id: p.pluginId, name: p.pluginName }));
plugins.value = parsePlugins;
} catch (e) {
pluginIds.value = [];
pluginDataList.value = [];
plugins.value = [];
}
}
//赋值
Object.assign(formState, data);
//根据知识库id查询知识库内容
@ -816,6 +943,10 @@
if (data.type === 'chatFLow' && data.flowId) {
getFlowDataById(data.flowId);
}
// 如果已有modelId查询模型信息并更新到metadata中
if (data.type === 'chatSimple' && data.modelId) {
handleModelIdChange(data.modelId);
}
}
//============= begin 提示词 ================================
@ -916,6 +1047,97 @@
formState.metadata = JSON.stringify(metadata.value);
}
// 检查是否有额外的工作流入参
const hasExtraFlowInputs = computed(() => {
if (!flowData.value || !flowData.value.metadata) {
return false;
}
try {
const flowInputsList = flowData.value.metadata || [];
// 过滤掉固定参数
const fixedParams = ['history', 'content', 'images'];
const extraInputs = flowInputsList.filter((input: any) => !fixedParams.includes(input.field));
return extraInputs.length > 0;
} catch (e) {
console.error('解析flowData.metadata失败', e);
return false;
}
});
// 监听flowData变化更新flowInputs
watch(
() => flowData.value,
(val) => {
if (!val || !val.metadata) {
flowInputs.value = [];
return;
}
try {
const flowInputsList = val.metadata || [];
flowInputs.value = flowInputsList;
} catch (e) {
console.error('解析flowData.metadata失败', e);
flowInputs.value = [];
}
},
{ immediate: true, deep: true }
);
/**
* 打开对话设置弹窗
*/
function handleEditSettings() {
if (settingsModalRef.value) {
settingsModalRef.value.open();
}
}
/**
* 对话设置确定回调
*/
function handleSettingsOk(settings: Record<string, any>) {
conversationSettings.value = settings;
console.log('调试模式对话设置已更新:', settings);
}
/**
* 模型ID变化处理
* 查询模型信息并更新到metadata中供chat组件使用
*/
async function handleModelIdChange(modelId: string) {
if (!modelId) {
// 如果清空模型,清除模型信息
if (metadata.value.modelInfo) {
delete metadata.value.modelInfo;
formState.metadata = JSON.stringify(metadata.value);
}
return;
}
try {
const res = await defHttp.get({
url: '/airag/airagModel/queryById',
params: { id: modelId }
}, { isTransformResponse: false });
if (res.success && res.result) {
const model = res.result;
// 将模型信息添加到metadata中
if (!metadata.value) {
metadata.value = {};
}
metadata.value.modelInfo = {
provider: model.provider || '',
modelType: model.modelType || '',
modelName: model.modelName || ''
};
formState.metadata = JSON.stringify(metadata.value);
}
} catch (e) {
console.error('获取模型信息失败', e);
}
}
return {
registerModal,
title,
@ -931,11 +1153,17 @@
wrapperCol,
validateInfos,
handleAddKnowledgeIdClick,
handleAddMcpClick,
registerKnowledgeModal,
registerMcpModal,
knowledgeDataList,
pluginDataList,
plugins,
knowledge,
handleSuccess,
handleMcpSuccess,
handleDeleteKnowledge,
handleDeleteMcp,
uuid,
prologueTextAreaBlur,
prologue,
@ -978,6 +1206,14 @@
metadata,
multiSessionChecked,
handleMultiSessionChange,
pluginIds,
settingsModalRef,
flowInputs,
conversationSettings,
hasExtraFlowInputs,
handleEditSettings,
handleSettingsOk,
handleModelIdChange,
};
},
};
@ -1186,6 +1422,14 @@
color: #757c8f;
margin-left: 10px;
}
.mcp-warning-tip {
display: flex;
align-items: center;
color: #fa8c16;
font-size: 12px;
line-height: 18px;
font-weight: 500;
}
.flow-icon{
width: 34px;
height: 34px;

View File

@ -0,0 +1,87 @@
import { defHttp } from '/@/utils/http/axios';
// import { useMessage } from "/@/hooks/web/useMessage"; // 需要确认弹窗再启用
enum Api {
list = '/airag/airagMcp/list',
save='/airag/airagMcp/save',
deleteOne = '/airag/airagMcp/delete',
importExcel = '/airag/airagMcp/importExcel',
exportXls = '/airag/airagMcp/exportXls',
sync = '/airag/airagMcp/sync',
toggleStatus = '/airag/airagMcp/status',
saveAndSync = '/airag/airagMcp/saveAndSync',
queryById = '/airag/airagMcp/queryById',
saveTools = '/airag/airagMcp/saveTools',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 删除单个
* @param params
* @param handleSuccess
*/
export const deleteOne = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
* 保存或者更新
* @param params
* @param isUpdate
*/
export const saveOrUpdate = (params) => {
return defHttp.post({url: Api.save, data: params}, { isTransformResponse: false });
}
/**
* 保存并同步
* @param params
* @param isUpdate
*/
export const saveAndSync = (params) => {
return defHttp.post({url: Api.saveAndSync, data: params}, { isTransformResponse: false });
}
/**
* 同步接口
* @param id
*/
export const syncMcp = (id) => defHttp.post({ url: Api.sync+"/"+id });
/**
* 修改状态
* @param id
*/
export const toggleStatus = (id,status) => defHttp.post({ url: Api.toggleStatus+"/"+id + "/"+ status });
/**
* 详情查询
* @param id
*/
export const queryById = (id) => defHttp.get({ url: Api.queryById ,params: { id:id }}, { isTransformResponse: false });
/**
* 保存插件工具仅更新tools字段
* @param id 插件ID
* @param tools 工具列表JSON字符串
*/
export const saveTools = (id, tools) => defHttp.post({ url: Api.saveTools, data: { id, tools } }, { isTransformResponse: false });

View File

@ -0,0 +1,78 @@
import {BasicColumn} from '/@/components/Table';
import {FormSchema} from '/@/components/Table';
import { rules} from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '图标',
align: "center",
dataIndex: 'icon'
},
{
title: '名称',
align: "center",
dataIndex: 'name'
},
{
title: '描述',
align: "center",
dataIndex: 'descr'
},
{
title: 'mcp类型ssesse类型stdio标准类型',
align: "center",
dataIndex: 'type'
},
{
title: '服务端点SSE类型为URLstdio类型为命令',
align: "center",
dataIndex: 'endpoint'
},
{
title: '请求头sse类型、环境变量stdio类型',
align: "center",
dataIndex: 'headers'
},
{
title: '工具列表',
align: "center",
dataIndex: 'tools'
},
{
title: '状态enable=启用、disable=禁用)',
align: "center",
dataIndex: 'status'
},
{
title: '是否同步',
align: "center",
dataIndex: 'synced'
},
{
title: '元数据',
align: "center",
dataIndex: 'metadata'
},
{
title: '租户id',
align: "center",
dataIndex: 'tenantId'
},
];
// 高级查询数据
export const superQuerySchema = {
icon: {title: '应用图标',order: 0,view: 'text', type: 'string',},
name: {title: '名称',order: 1,view: 'text', type: 'string',},
descr: {title: '描述',order: 2,view: 'text', type: 'string',},
type: {title: 'mcp类型ssesse类型stdio标准类型',order: 3,view: 'text', type: 'string',},
endpoint: {title: '服务端点SSE类型为URLstdio类型为命令',order: 4,view: 'textarea', type: 'string',},
headers: {title: '请求头sse类型、环境变量stdio类型',order: 5,view: 'textarea', type: 'string',},
tools: {title: '工具列表',order: 6,view: 'textarea', type: 'string',},
status: {title: '状态enable=启用、disable=禁用)',order: 7,view: 'text', type: 'string',},
synced: {title: '是否同步',order: 8,view: 'number', type: 'number',},
metadata: {title: '元数据',order: 9,view: 'textarea', type: 'string',},
tenantId: {title: '租户id',order: 10,view: 'text', type: 'string',},
};

View File

@ -0,0 +1,544 @@
<template>
<div class="mcp">
<!-- 查询区域 -->
<div class="jeecg-basic-table-form-container">
<a-form
ref="formRef"
@keyup.enter="searchQuery"
:model="queryParam"
:label-col="labelCol"
:wrapper-col="wrapperCol"
style="background-color: #f7f8fc !important;"
>
<a-row :gutter="24">
<a-col :lg="6">
<a-form-item name="name" label="名称">
<JInput v-model:value="queryParam.name" />
</a-form-item>
</a-col>
<a-col :lg="6">
<a-form-item name="category" label="类型">
<a-select v-model:value="queryParam.category" placeholder="全部" allowClear>
<a-select-option value="plugin">插件</a-select-option>
<a-select-option value="mcp">MCP</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xl="6" :lg="7" :md="8" :sm="24">
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
<a-col :lg="6">
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
</a-col>
</span>
</a-col>
</a-row>
</a-form>
</div>
<!-- 卡片区域 -->
<a-row :span="24" class="mcp-row">
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24">
<a-card class="add-mcp-card" @click="handleAdd" >
<div class="flex">
<Icon icon="ant-design:plus-outlined" class="add-mcp-card-icon" size="20" />
<span class="add-mcp-card-title">新增MCP</span>
</div>
</a-card>
</a-col>
<a-col v-for="item in mcpList" :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24" :key="item.id" v-if="mcpList && mcpList.length > 0">
<a-card class="mcp-card" @click="handleDetailClick(item)">
<div class="mcp-header">
<div class="flex">
<img :src="getIcon(item.icon)" class="header-img" />
<div class="header-text" :title="item.name">{{ item.name }}</div>
</div>
</div>
<div class="mt-6">
<ul>
<li class="flex mr-14">
<span class="described" :title="item.descr">{{ item.descr || '-' }}</span>
</li>
</ul>
</div>
<div class="mcp-btn">
<a-button class="mcp-icon" size="small" @click.prevent.stop="handleEditClick(item)" v-auth="'llm:airag_mcp:edit'">
<Icon icon="ant-design:edit-outlined" />
</a-button>
<a-dropdown placement="bottomRight" :trigger="['click']" :getPopupContainer="(node) => node.parentNode">
<div class="ant-dropdown-link pointer mcp-icon" @click.prevent.stop>
<Icon icon="ant-design:ellipsis-outlined" />
</div>
<template #overlay>
<a-menu>
<!-- MCP类型显示同步按钮 -->
<a-menu-item
v-if="item.category === 'mcp'"
key="sync"
@click.prevent.stop="handleSync(item)"
v-auth="'llm:airag_mcp:sync'"
>
<Icon icon="ant-design:cloud-sync-outlined" size="16" /> 同步
</a-menu-item>
<!-- 插件类型显示工具管理按钮 -->
<a-menu-item
v-if="item.category === 'plugin'"
key="toolManage"
@click.prevent.stop="handleToolManage(item)"
v-auth="'llm:airag_mcp:edit'"
>
<Icon icon="ant-design:tool-outlined" size="16" /> 工具管理
</a-menu-item>
<!-- 编辑始终显示不受禁用启用影响 -->
<a-menu-item
key="edit"
@click.prevent.stop="handleEditClick(item)"
v-auth="'llm:airag_mcp:edit'"
>
<Icon icon="ant-design:edit-outlined" size="16" /> 编辑
</a-menu-item>
<a-menu-item
v-if="item.synced"
key="toggle"
@click.prevent.stop="handleToggleStatus(item)"
v-auth="'llm:airag_mcp:edit'"
>
<Icon :icon="item.status === 'enable' ? 'ant-design:stop-outlined' : 'ant-design:check-circle-outlined'" size="16" />
{{ item.status === 'enable' ? '禁用' : '启用' }}
</a-menu-item>
<a-menu-item
v-if="item.status === 'disable' || !item.synced"
key="delete"
@click.prevent.stop="handleDeleteClick(item)"
v-auth="'llm:airag_mcp:delete'"
>
<Icon icon="ant-design:delete-outlined" size="16" /> 删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div class="card-footer" v-if="true">
<div class="pill type-pill" :title="'类型: '+(item.category === 'plugin' ? '插件' : 'MCP')">
<Icon :icon="getCategoryIcon(item.category)" class="pill-icon" />
<span class="pill-text">{{ item.category === 'plugin' ? '插件' : 'MCP' }}</span>
</div>
<div class="pill status-pill" :class="item.synced ? (item.status==='enable'?'status-enable-pill':'status-disable-pill'):'status-unsynced-pill'" :title="item.synced ? (item.status==='enable'?'已启用':'未启用'):'未同步'">
<Icon :icon="getStatusIcon(item)" class="pill-icon" />
<span class="pill-text">{{ item.synced ? (item.status==='enable'?'启用':'禁用') : '未同步' }}</span>
</div>
<div class="pill tool-pill" :title="getToolCount(item.metadata)+' 个工具'">
<Icon icon="ant-design:tool-outlined" class="pill-icon" />
<span class="pill-text">{{ getToolCount(item.metadata) }} 个工具</span>
</div>
</div>
</a-card>
</a-col>
</a-row>
<Pagination
v-if="mcpList.length > 0"
:current="pageNo"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:total="total"
:showQuickJumper="true"
:showSizeChanger="true"
@change="handlePageChange"
class="list-footer"
size="small"
:show-total="() => `${total}` "
/>
</div>
<!-- 弹窗区域 -->
<AiragMcpAddModal @register="registerModal" @success="reload" />
<AiragMcpDetailModal @register="registerDetailModal" @edit="handleDetailEdit" @success="reload" />
</template>
<script lang="ts" name="llm-airagMcp" setup>
import { reactive, ref } from 'vue';
import { Pagination } from 'ant-design-vue';
import AiragMcpAddModal from './components/AiragMcpAddModal.vue';
import AiragMcpDetailModal from './components/AiragMcpDetailModal.vue';
import { list, deleteOne, syncMcp, toggleStatus} from './AiragMcp.api';
import { useModal } from '/@/components/Modal';
import JInput from '@/components/Form/src/jeecg/components/JInput.vue';
import defaultLogo from './imgs/mcpLogo.png'
// 列表数据
const mcpList = ref<any[]>([]);
// 分页
const pageNo = ref<number>(1);
const pageSize = ref<number>(10);
const total = ref<number>(0);
const pageSizeOptions = ref<any>(['10', '20', '30']);
// 查询参数
const queryParam = reactive<any>({});
const formRef = ref();
// 查询区域label宽度
const labelCol = reactive({
xs: 24,
sm: 4,
xl: 6,
xxl: 6,
});
const wrapperCol = reactive({
xs: 24,
sm: 20,
});
// 弹窗(新增/编辑)
const [registerModal, { openModal }] = useModal();
// 详情弹窗
const [registerDetailModal, { openModal: openDetailModal }] = useModal();
// 初始化
reload();
function reload() {
const params: any = {
pageNo: pageNo.value,
pageSize: pageSize.value,
column: 'createTime',
order: 'desc',
...queryParam,
};
list(params).then((res) => {
if (res.records) {
mcpList.value = res.records;
total.value = res.total;
} else {
mcpList.value = [];
total.value = 0;
}
});
}
function handlePageChange(page, current) {
pageNo.value = page;
pageSize.value = current;
reload();
}
function handleAdd() {
openModal(true, {});
}
function handleEditClick(item) {
// 参考模型列表,仅传 id如需后端查询可在弹窗内部扩展
openModal(true, { id: item.id, ...item });
}
function handleDetailClick(item){
// 仅传 id详情弹窗内部调用 queryById 获取最新数据
openDetailModal(true, { id: item.id });
}
function handleDetailEdit(record){
// 从详情弹窗内部触发编辑
openModal(true, { id: record.id, ...record });
}
async function handleDeleteClick(item) {
if (mcpList.value.length === 1 && pageNo.value > 1) {
pageNo.value = pageNo.value - 1;
}
await deleteOne({ id: item.id }, reload);
}
async function handleSync(item) {
await syncMcp(item.id).finally(() => reload());
}
async function handleToggleStatus(item) {
const newStatus = item.status === 'enable' ? 'disable' : 'enable';
await toggleStatus(item.id , newStatus).finally(() => reload());
}
function searchQuery() {
pageNo.value = 1;
reload();
}
function searchReset() {
formRef.value?.resetFields();
Object.keys(queryParam).forEach((k) => (queryParam[k] = ''));
searchQuery();
}
// 图标处理(如果 icon 是完整URL则使用否则可以扩展映射
function getIcon(icon?: string) {
if (!icon) return defaultLogo;
return icon.startsWith('http') ? icon : icon; // 可扩展为本地静态资源路径
}
// 工具数量:从 metadata 中读取 tool_count可处理对象或 JSON 字符串
function getToolCount(metadata: any): number {
if (!metadata) return 0;
let metaObj: any = metadata;
if (typeof metadata === 'string') {
try {
metaObj = JSON.parse(metadata);
} catch (e) {
return 0;
}
}
const count = metaObj.tool_count || metaObj.toolCount || 0;
return typeof count === 'number' ? count : parseInt(count, 10) || 0;
}
// 类型图标映射
function getTypeIcon(type?: string) {
switch (type) {
case 'sse':
return 'ant-design:thunderbolt-outlined';
case 'stdio':
return 'ant-design:code-outlined';
default:
return 'ant-design:appstore-outlined';
}
}
// category图标映射
function getCategoryIcon(category?: string) {
if (category === 'plugin') {
return 'ant-design:api-outlined';
}
return 'ant-design:tool-twotone';
}
// 工具管理 - 打开详情页面
function handleToolManage(item: any) {
openDetailModal(true, { id: item.id });
}
// 状态/同步图标
function getStatusIcon(item: any) {
if (!item.synced) return 'ant-design:cloud-sync-outlined';
return item.status === 'enable' ? 'ant-design:check-circle-outlined' : 'ant-design:stop-outlined';
}
// <script setup> 下自动暴露到模板, 无需 export
</script>
<style lang="less" scoped>
.mcp {
height: calc(100vh - 115px);
background: #f7f8fc;
padding: 24px;
overflow-y: auto;
.mcp-row {
/* 允许阴影完整显示 */
margin-top: 20px;
padding-bottom: 12px;
overflow: visible;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: 20px;
:deep(.ant-col) { flex: 0 0 270px; max-width: 270px; }
.mcp-card, .add-mcp-card { width: 270px; }
.mcp-header {
position: relative;
font-size: 14px;
.header-img {
width: 32px;
height: 32px;
margin-right: 12px;
}
.header-text {
font-size: 16px;
font-weight: bold;
color: #354052;
width: calc(100% - 80px);
overflow: hidden;
align-content: center;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.label {
font-weight: 400;
font-size: 12px;
align-self: center;
color: #8a8f98;
overflow-wrap: break-word;
}
.no-activate {
font-size: 10px;
color: #ff4d4f;
border: 1px solid #ff4d4f;
border-radius: 10px;
padding: 0 6px;
height: 14px;
line-height: 12px;
margin-left: 6px;
align-self: center;
}
.described {
font-weight: 400;
margin-left: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
/* Fallback for supporting browsers */
line-clamp: 3;
overflow: hidden;
font-size: 12px;
max-width: 100%;
line-height: 18px;
color: #848b99;
}
.status-enable {
color: #52c41a;
}
.status-disable {
color: #ff4d4f;
}
.status-unsynced {
color: #fa8c16;
}
.flex {
display: flex;
}
:deep(.ant-card .ant-card-body) {
padding: 16px;
}
.mr-14 {
margin-right: 14px;
}
.mt-6 {
margin-top: 6px;
}
.ml-4 {
margin-left: 4px;
}
.mcp-btn {
position: absolute;
right: 4px;
top: 6px;
height: auto;
display: none;
}
.mcp-card {
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px #e6e6e6;
transition: all 0.3s ease;
border-radius: 10px;
height: 152px;
cursor: pointer;
position: relative;
}
.mcp-card:hover {
box-shadow: 0 6px 12px #d0d3d8;
.mcp-btn {
display: flex;
}
}
.tool-count {
position: absolute;
left: 16px;
bottom: 12px;
font-size: 12px;
color: #4e5969;
display: flex;
align-items: center;
background: rgba(245,246,247,0.9);
padding: 2px 8px;
border-radius: 12px;
line-height: 18px;
}
.card-footer {
position: absolute;
left: 16px;
bottom: 12px;
display: flex;
flex-wrap: nowrap;
gap: 8px;
}
.pill {
display: inline-flex;
align-items: center;
padding: 2px 10px 2px 8px;
border-radius: 14px;
font-size: 12px;
line-height: 18px;
font-weight: 500;
backdrop-filter: saturate(180%) blur(4px);
box-shadow: 0 0 0 1px rgba(0,0,0,0.05);
.pill-icon { margin-right: 4px; }
}
.type-pill { background: linear-gradient(135deg,#e6f4ff,#f0f9ff); color:#0958d9; }
.status-enable-pill { background: linear-gradient(135deg,#e8f9e9,#f0fff0); color:#2f952f; }
.status-disable-pill { background: linear-gradient(135deg,#fff1f0,#ffecec); color:#c43826; }
.status-unsynced-pill { background: linear-gradient(135deg,#fff7e6,#fff3d9); color:#d46b08; }
.tool-pill { background: linear-gradient(135deg,#f5f6f7,#f0f1f2); color:#555; }
.pointer {
cursor: pointer;
}
.list-footer {
text-align: right;
margin-top: 5px;
}
.jeecg-basic-table-form-container {
padding: 0;
:deep(.ant-form) {
background-color: transparent;
}
.table-page-search-submitButtons {
display: block;
margin-bottom: 24px;
white-space: nowrap;
}
}
.add-mcp-card {
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px #e6e6e6;
transition: all 0.3s ease;
border-radius: 10px;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 16px;
cursor: pointer;
height: 152px;
width: 270px;
.add-mcp-card-icon {
padding: 8px;
color: #1f2329;
background-color: #f5f6f7;
margin-right: 12px;
}
.add-mcp-card-title {
font-size: 16px;
color: #1f2329;
font-weight: 400;
align-self: center;
}
}
.add-mcp-card:hover {
box-shadow: 0 6px 12px #d0d3d8;
}
.mcp-icon {
background-color: unset;
border: none;
margin-right: 2px;
}
.mcp-icon:hover {
color: #000000;
background-color: #e9ecf2;
border: none;
}
.ant-dropdown-link {
font-size: 14px;
height: 24px;
padding: 0 7px;
border-radius: 4px;
align-content: center;
text-align: center;
}
</style>

View File

@ -0,0 +1,545 @@
<template>
<BasicModal
destroyOnClose
@register="registerModal"
:canFullscreen="false"
width="640px"
:title="isEdit ? '编辑MCP' : '新增MCP'"
wrapClassName="airag-mcp-modal"
>
<div class="modal">
<div class="mcp-content">
<BasicForm @register="registerForm">
<!-- category类型选择单选 -->
<template #category="{ model, field }">
<a-radio-group v-model:value="model[field]" @change="onCategoryChange" :disabled="isEdit">
<a-radio value="mcp">MCP</a-radio>
<a-radio value="plugin">插件</a-radio>
</a-radio-group>
</template>
<!-- MCP类型选择 -->
<!-- <template #type="{ model, field }">-->
<!-- <a-select v-model:value="model[field]" @change="onTypeChange">-->
<!-- <a-select-option value="sse">SSE</a-select-option>-->
<!-- <a-select-option value="stdio">STDIO</a-select-option>-->
<!-- </a-select>-->
<!-- </template>-->
<!-- endpoint 根据类型切换 placeholder -->
<template #endpoint="{ model, field }">
<a-input
v-model:value="model[field]"
:placeholder="endpointPlaceholder"
/>
</template>
<!-- headers 根据类型切换标签名称与 placeholder -->
<template #headers>
<div class="headers-table-wrapper">
<a-button type="primary" size="small" @click="addHeaderRow" style="margin-bottom: 8px;">
添加
</a-button>
<div class="headers-table-container">
<a-table
:dataSource="headersData"
:columns="headersColumns"
:pagination="false"
size="small"
bordered
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'key'">
<a-input v-model:value="record.key" placeholder="请输入键" size="small" />
</template>
<template v-if="column.key === 'value'">
<a-input v-model:value="record.value" placeholder="请输入值" size="small" />
</template>
<template v-if="column.key === 'action'">
<a-button type="link" danger size="small" @click="deleteHeaderRow(index)">删除</a-button>
</template>
</template>
</a-table>
</div>
</div>
</template>
<!-- 授权方式配置仅插件类型 -->
<template #authType="{ model, field }">
<a-select v-model:value="model[field]" @change="onAuthTypeChange">
<a-select-option value="none">不需要授权</a-select-option>
<a-select-option value="token">Token / API Key</a-select-option>
</a-select>
</template>
<!-- Token参数名仅插件类型且选择Token授权时 -->
<template #tokenParamName="{ model, field }">
<a-input v-model:value="model[field]" placeholder="请输入Token参数名" />
</template>
<!-- Token参数值仅插件类型且选择Token授权时 -->
<template #tokenParamValue="{ model, field }">
<a-input v-model:value="model[field]" type="password" placeholder="请输入Token值" />
</template>
</BasicForm>
</div>
</div>
<template #footer>
<a-button @click="close">关闭</a-button>
<a-button type="primary" @click="handleSubmit" :loading="submitLoading">保存</a-button>
<a-button v-if="categoryValue === 'mcp'" type="primary" @click="handleSubmitAndSync" :loading="submitLoading">保存并同步</a-button>
</template>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, nextTick } from 'vue';
import { useModalInner } from '/@/components/Modal';
import BasicModal from '/@/components/Modal/src/BasicModal.vue';
import BasicForm from '/@/components/Form/src/BasicForm.vue';
import { useForm } from '/@/components/Form';
import { saveOrUpdate, saveAndSync } from '../AiragMcp.api';
import { useMessage } from '/@/hooks/web/useMessage';
const emit = defineEmits(['success', 'register']);
const { createMessage } = useMessage();
const isEdit = ref(false);
const submitLoading = ref(false);
const recordId = ref<string | undefined>(undefined);
// category类型plugin/mcp
const categoryValue = ref('mcp');
// MCP类型sse/stdio
const typeValue = ref('sse');
// 授权方式
const authType = ref('none'); // none: 不需要授权, token: Token / API Key
// Token配置
const tokenParamName = ref('X-Access-Token');
const tokenParamValue = ref('');
const endpointPlaceholder = computed(() => {
if (categoryValue.value === 'plugin') {
return '请输入BaseURL例如https://api.example.com可选不填使用当前系统地址';
}
return typeValue.value === 'sse' ? '请输入服务端点URL例如https://example.com/stream' : '请输入启动命令,例如:./start-mcp-service';
});
// 表格数据
const headersData = ref<Array<{ key: string; value: string }>>([]);
// 表格列配置
const headersColumns = [
{ title: '键', dataIndex: 'key', key: 'key', width: '40%' },
{ title: '值', dataIndex: 'value', key: 'value', width: '45%' },
{ title: '操作', key: 'action', width: '15%' },
];
// 添加行
function addHeaderRow() {
headersData.value.push({ key: '', value: '' });
}
// 删除行
function deleteHeaderRow(index: number) {
// 如果只剩一行
if (headersData.value.length <= 1) {
const lastRow = headersData.value[0];
// 判断最后一行是否为空
if (!lastRow.key && !lastRow.value) {
// 如果键和值都是空的,不处理
return;
} else {
// 如果不是空的,清空这一行的键和值
lastRow.key = '';
lastRow.value = '';
createMessage.success('已清空数据');
return;
}
}
headersData.value.splice(index, 1);
}
// 将表格数据转换为JSON字符串格式
function headersDataToString(): string {
const filtered = headersData.value.filter(item => item.key && item.value);
if (filtered.length === 0) {
return '';
}
const obj = filtered.reduce((acc, item) => {
acc[item.key] = item.value;
return acc;
}, {} as Record<string, string>);
return JSON.stringify(obj);
}
// 将JSON字符串格式转换为表格数据
function stringToHeadersData(str: string | undefined) {
if (!str) {
// 默认添加一行空数据
headersData.value = [{ key: '', value: '' }];
return;
}
try {
const obj = JSON.parse(str);
const entries = Object.entries(obj);
if (entries.length === 0) {
// 如果解析后没有数据,默认添加一行空数据
headersData.value = [{ key: '', value: '' }];
} else {
headersData.value = entries.map(([key, value]) => ({
key,
value: String(value)
}));
}
} catch (e) {
// JSON解析失败默认添加一行空数据
console.error('解析headers JSON失败:', e);
headersData.value = [{ key: '', value: '' }];
}
}
function onAuthTypeChange(val: string) {
authType.value = val;
updateSchema([
{ field: 'tokenParamName', required: val === 'token', show: val === 'token' },
{ field: 'tokenParamValue', required: false, show: val === 'token' },
]);
}
function onCategoryChange(val) {
categoryValue.value = val.target.value;
if (categoryValue.value === 'plugin') {
// 插件类型显示BaseURL、请求头、授权方式隐藏MCP类型选择
updateSchema([
{ field: 'category', label: '类型' },
{ field: 'endpoint', label: 'BaseURL', required: false },
{ field: 'type', show: false },
{ field: 'headers', label: '请求头', show: true },
{ field: 'authType', label: '授权方式', required: true, show: true },
{ field: 'tokenParamName', label: 'Token参数名', required: authType.value === 'token', show: authType.value === 'token' },
{ field: 'tokenParamValue', label: 'Token参数值', required: false , show: authType.value === 'token' },
]);
// 设置插件类型的默认值
setFieldsValue({
type: 'api',
authType: authType.value || 'none',
tokenParamName: tokenParamName.value || 'X-Access-Token',
});
} else {
// MCP类型显示原有字段
updateSchema([
{ field: 'category', label: '类型' },
{ field: 'endpoint', label: typeValue.value === 'sse' ? 'URL' : '命令', required: true },
{ field: 'type', label: 'MCP类型', show: false },
{ field: 'headers', label: typeValue.value === 'sse' ? '请求头(目前不支持)' : '环境变量', show: false },
{ field: 'authType', show: false },
{ field: 'tokenParamName', show: false },
{ field: 'tokenParamValue', show: false },
]);
// 设置MCP类型的默认值
setFieldsValue({
type: typeValue.value || 'sse',
authType: undefined,
tokenParamName: undefined,
tokenParamValue: undefined,
});
}
}
// 表单配置
const [registerForm, { resetFields, validate, setFieldsValue, updateSchema }] = useForm({
showActionButtonGroup: false,
layout: 'vertical',
baseColProps: { span: 24 },
schemas: [
{ field: 'name', component: 'Input', label: '名称', required: true, componentProps: { placeholder: '请输入名称' } },
{ field: 'icon', label: '图标', component: 'JImageUpload' },
{ field: 'category', component: 'RadioGroup', label: '类型', required: true, slot: 'category', defaultValue: 'mcp' },
{ field: 'type', component: 'Select', label: 'MCP类型', required: false, show: false, slot: 'type', defaultValue: 'sse' },
{ field: 'endpoint', component: 'Input', label: 'URL', required: true, slot: 'endpoint' },
{ field: 'headers', component: 'InputTextArea', label: '请求头', slot: 'headers', show: false },
{ field: 'authType', component: 'Select', label: '授权方式', required: true, slot: 'authType', defaultValue: 'none', show: false },
{ field: 'tokenParamName', component: 'Input', label: 'Token参数名', required: false, slot: 'tokenParamName', defaultValue: 'X-Access-Token', show: false },
{ field: 'tokenParamValue', component: 'Input', label: 'Token参数值', required: false, slot: 'tokenParamValue', show: false },
{ field: 'descr', component: 'InputTextArea', label: '描述', componentProps: { rows: 3, placeholder: '请输入描述' } },
]
});
const [registerModal, { closeModal }] = useModalInner(async (data) => {
await resetFields();
submitLoading.value = false;
recordId.value = data?.id;
if (data && Object.keys(data).length > 0) {
// 区分新增/编辑
if (data.id) {
isEdit.value = true;
} else {
isEdit.value = false;
}
// 获取category默认为mcp
const category = data.category || 'mcp';
categoryValue.value = category;
const t = data.type || 'sse';
typeValue.value = t;
// 解析授权配置从headers或metadata中解析
let parsedAuthType = 'none';
let parsedTokenParamName = 'X-Access-Token';
let parsedTokenParamValue = '';
if (category === 'plugin') {
// 尝试从metadata中解析授权配置
if (data.metadata) {
try {
const metadata = typeof data.metadata === 'string' ? JSON.parse(data.metadata) : data.metadata;
parsedAuthType = metadata.authType || 'none';
parsedTokenParamName = metadata.tokenParamName || 'X-Access-Token';
parsedTokenParamValue = metadata.tokenParamValue || '';
} catch (e) {
// 解析失败,使用默认值
}
}
// 从headers中提取token如果存在
let headersObj: any = {};
if (data.headers) {
try {
headersObj = typeof data.headers === 'string' ? JSON.parse(data.headers) : data.headers;
// 如果metadata中有token配置尝试从headers中提取对应的值
if (parsedAuthType === 'token' && parsedTokenParamName && headersObj[parsedTokenParamName]) {
parsedTokenParamValue = headersObj[parsedTokenParamName];
// 从headers中移除token避免在表格中显示
delete headersObj[parsedTokenParamName];
}
} catch (e) {
// 解析失败
}
}
authType.value = parsedAuthType;
tokenParamName.value = parsedTokenParamName;
tokenParamValue.value = parsedTokenParamValue;
updateSchema([
{ field: 'category', label: '类型', componentProps: { disabled: true } },
{ field: 'endpoint', label: 'BaseURL', required: false },
{ field: 'type', show: false },
{ field: 'headers', label: '请求头', show: true },
{ field: 'authType', label: '授权方式', required: true, show: true },
{ field: 'tokenParamName', label: 'Token参数名', required: parsedAuthType === 'token', show: parsedAuthType === 'token' },
{ field: 'tokenParamValue', label: 'Token参数值', required: false, show: parsedAuthType === 'token' },
]);
// 将处理后的headers已移除token转换为表格数据
stringToHeadersData(Object.keys(headersObj).length > 0 ? JSON.stringify(headersObj) : '');
// 需要在下一个tick设置值确保updateSchema完成
nextTick(() => {
setFieldsValue({
icon: data.icon,
name: data.name,
descr: data.descr,
category: category,
type: t,
endpoint: data.endpoint,
authType: parsedAuthType,
tokenParamName: parsedTokenParamName,
tokenParamValue: parsedTokenParamValue,
});
});
} else {
updateSchema([
{ field: 'category', label: '类型', componentProps: { disabled: true } },
{ field: 'endpoint', label: t === 'sse' ? 'URL' : '命令', required: true },
{ field: 'type', label: 'MCP类型', show: false },
{ field: 'headers', label: t === 'sse' ? '请求头(目前不支持)' : '环境变量', show: false },
{ field: 'authType', show: false },
{ field: 'tokenParamName', show: false },
{ field: 'tokenParamValue', show: false },
]);
// 将 headers 字符串转换为表格数据
stringToHeadersData(data.headers);
// 需要在下一个tick设置值确保updateSchema完成
nextTick(() => {
setFieldsValue({
icon: data.icon,
name: data.name,
descr: data.descr,
category: category,
type: t,
endpoint: data.endpoint,
});
});
}
} else {
isEdit.value = false;
categoryValue.value = 'mcp';
typeValue.value = 'sse';
authType.value = 'none';
tokenParamName.value = 'X-Access-Token';
tokenParamValue.value = '';
// 默认添加一行空数据
headersData.value = [{ key: '', value: '' }];
updateSchema([
{ field: 'category', label: '类型' },
{ field: 'endpoint', label: 'URL' },
{ field: 'headers', label: '请求头', show: false },
{ field: 'authType', show: false },
{ field: 'tokenParamName', show: false },
{ field: 'tokenParamValue', show: false },
]);
// 设置默认选中值
setFieldsValue({ category: 'mcp', type: 'sse', authType: 'none', tokenParamName: 'X-Access-Token', tokenParamValue: '' });
}
});
async function handleSubmit(){
try {
submitLoading.value = true;
const values = await validate();
if(recordId.value){
values.id = recordId.value;
}
if (values.category === 'plugin') {
// 插件类型:验证授权配置
if (values.authType === 'token') {
if (!values.tokenParamName) {
createMessage.error('Token授权方式需要填写Token参数名和参数值');
return;
}
}
// 插件类型处理headers和授权配置
// 合并headers和token配置
let headersObj = {};
const headersStr = headersDataToString();
if (headersStr) {
try {
headersObj = JSON.parse(headersStr);
} catch (e) {
headersObj = {};
}
}
// 如果选择了Token授权添加token到headers
if (values.authType === 'token' && values.tokenParamName && values.tokenParamValue) {
headersObj[values.tokenParamName] = values.tokenParamValue;
}
values.headers = Object.keys(headersObj).length > 0 ? JSON.stringify(headersObj) : '';
// 将授权配置保存到metadata
const metadata: any = {};
if (values.metadata) {
try {
Object.assign(metadata, typeof values.metadata === 'string' ? JSON.parse(values.metadata) : values.metadata);
} catch (e) {
// 解析失败,使用空对象
}
}
metadata.authType = values.authType || 'none';
if (values.authType === 'token') {
metadata.tokenParamName = values.tokenParamName || 'X-Access-Token';
metadata.tokenParamValue = values.tokenParamValue || '';
}
values.metadata = JSON.stringify(metadata);
values.type = 'api';
} else {
// MCP类型将表格数据转换为字符串
values.headers = headersDataToString();
// 清除授权相关字段
delete values.authType;
delete values.tokenParamName;
delete values.tokenParamValue;
}
const res = await saveOrUpdate(values);
if(res.success){
createMessage.success(res.message || '保存成功');
emit('success');
closeModal();
}else{
createMessage.error(res.message || '保存失败');
}
}finally{
submitLoading.value = false;
}
}
async function handleSubmitAndSync(){
try {
submitLoading.value = true;
const values = await validate();
if(recordId.value){
values.id = recordId.value;
}
// 将表格数据转换为字符串
values.headers = headersDataToString();
const res = await saveAndSync(values);
if(res.success){
createMessage.success(res.message || '保存并同步成功');
emit('success');
closeModal();
}else{
createMessage.error(res.message || '保存并同步失败');
}
}finally{
submitLoading.value = false;
}
}
function close(){
closeModal();
}
</script>
<style scoped lang="less">
.modal {
padding: 5px 16px 8px 16px;
.header {
padding: 0 0 12px 0;
display: flex;
justify-content: space-between;
.header-title {
font-size: 16px;
font-weight: bold;
}
}
.mcp-content {
:deep(.ant-form-item) { margin-bottom: 8px; }
:deep(.ant-input),
:deep(.ant-input-number),
:deep(.ant-select),
:deep(.ant-select-selector),
:deep(.ant-textarea),
:deep(textarea.ant-input) { width: 100% !important; }
:deep(.ant-select-selector){ padding: 0 8px; }
.headers-table-wrapper {
.headers-table-container {
max-height: 200px;
overflow-y: auto;
border: 1px solid #d9d9d9;
border-radius: 2px;
:deep(.ant-table) {
.ant-table-thead > tr > th {
background: #fafafa;
padding: 8px;
}
.ant-table-tbody > tr > td {
padding: 4px 8px;
}
}
}
}
}
}
</style>
<style lang="less">
.airag-mcp-modal {
.jeecg-basic-modal-close > span { margin-left: 0 !important; }
.ant-modal {
max-height: 85vh;
.ant-modal-body {
max-height: calc(85vh - 110px);
overflow-y: auto;
}
}
}
</style>

View File

@ -0,0 +1,614 @@
<template>
<BasicModal
destroyOnClose
@register="registerModal"
:canFullscreen="false"
width="720px"
wrapClassName="airag-mcp-detail-modal"
>
<template #title>
<div class="detail-modal-title">
<span>详情</span>
<a-button
class="detail-edit-btn"
type="text"
@click="emitEdit"
:title="'编辑'"
>
<Icon icon="ant-design:edit-outlined" :size="18" />
</a-button>
</div>
</template>
<div class="detail-modal" v-loading="loading">
<div class="detail-header">
<img :src="displayIcon" class="detail-icon" />
<div class="detail-titles">
<div class="detail-name" :title="record?.name">{{ record?.name || '-' }}</div>
<div class="detail-type-status">
<a-tag color="blue">{{ record?.category === 'plugin' ? '插件' : (record?.type || '未知类型') }}</a-tag>
<a-tag v-if="record?.synced" :color="record?.status === 'enable' ? 'green' : 'red'">
{{ record?.status === 'enable' ? '已启用' : '未启用' }}
</a-tag>
<a-tag v-else color="orange">未同步</a-tag>
</div>
</div>
</div>
<div class="detail-descr" :title="record?.descr">{{ record?.descr || '暂无描述' }}</div>
<div class="tools-wrapper">
<div class="tools-header">
<div class="tools-title">工具列表</div>
<a-button v-if="record?.category === 'plugin'" type="primary" size="small" @click="handleAddTool">添加工具</a-button>
</div>
<div class="tools-grid" v-if="(pluginTools && pluginTools.length) || (tools && tools.length)">
<!-- 插件类型工具 -->
<template v-if="record?.category === 'plugin'">
<div
v-for="tool in pluginTools"
:key="tool.name"
class="tool-item"
@click="handleEditTool(tool)"
>
<div class="tool-header-item" @click.stop>
<div class="tool-name" :title="tool.name">{{ tool.name }}</div>
<div class="tool-actions">
<a-switch
v-model:checked="tool.enabled"
size="small"
@change="handleToolEnabledChange(tool)"
@click.stop
/>
<a-button
type="text"
size="small"
@click.stop="handleEditTool(tool)"
:title="'编辑工具'"
>
<Icon icon="ant-design:edit-outlined" :size="16" />
</a-button>
<a-button
type="text"
danger
size="small"
@click.stop="handleDeleteTool(tool)"
:title="'删除工具'"
>
<Icon icon="ant-design:delete-outlined" :size="16" />
</a-button>
</div>
</div>
<div class="tool-descr" :title="tool.description">{{ tool.description || '无描述' }}</div>
<div v-if="tool.method || tool.path" class="tool-meta">
<a-tag v-if="tool.method" size="small" :color="getMethodColor(tool.method)">{{ tool.method }}</a-tag>
<span v-if="tool.path" class="tool-path">{{ tool.path }}</span>
</div>
</div>
</template>
<!-- MCP类型工具 -->
<template v-else>
<div
v-for="tool in tools"
:key="tool.name"
class="tool-item"
>
<div class="tool-header-item" @click.stop>
<div class="tool-name" :title="tool.name">{{ tool.name }}</div>
</div>
<div class="tool-descr" :title="tool.descr">{{ tool.descr || '无描述' }}</div>
</div>
</template>
</div>
<a-empty v-else description="暂无工具" />
</div>
</div>
<template #footer>
<a-button @click="closeModal()">关闭</a-button>
</template>
</BasicModal>
<!-- 工具编辑弹窗 -->
<PluginToolEditModal @register="registerToolEditModal" @success="handleToolEditSuccess" />
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { useModalInner, useModal } from '/@/components/Modal';
import BasicModal from '/@/components/Modal/src/BasicModal.vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { Modal } from 'ant-design-vue';
import { queryById } from '../AiragMcp.api';
import defaultLogo from '../imgs/mcpLogo.png';
import PluginToolEditModal from './PluginToolEditModal.vue';
const { createMessage } = useMessage();
interface McpToolItem {
name: string;
descr?: string;
enabled?: boolean;
}
interface PluginToolItem {
name: string;
description?: string;
path?: string;
method?: string;
parameters?: any[];
responses?: any[];
enabled?: boolean;
}
interface McpRecord {
id?: string;
name?: string;
descr?: string;
icon?: string;
type?: string;
category?: string;
status?: string;
synced?: boolean;
tools?: McpToolItem[] | string;
endpoint?: string;
headers?: string;
metadata?: string | any;
}
const emit = defineEmits(['register','edit', 'success']);
const record = ref<McpRecord | null>(null);
const tools = ref<McpToolItem[]>([]);
const pluginTools = ref<PluginToolItem[]>([]);
const loading = ref<boolean>(false);
// 工具编辑弹窗
const [registerToolEditModal, { openModal: openToolEditModal }] = useModal();
const displayIcon = computed(() => {
const icon = record.value?.icon;
if (!icon) return defaultLogo;
return icon.startsWith('http') ? icon : icon;
});
const [registerModal, { closeModal }] = useModalInner(async (data: McpRecord) => {
if(!data?.id){
record.value = { ...data };
// 根据category初始化工具列表
if (data.category === 'plugin') {
try {
const toolsData = typeof data.tools === 'string' ? JSON.parse(data.tools) : data.tools;
pluginTools.value = Array.isArray(toolsData) ? toolsData : [];
} catch (e) {
pluginTools.value = [];
}
tools.value = [];
} else {
tools.value = Array.isArray(data.tools) ? data.tools : [];
pluginTools.value = [];
}
return;
}
loading.value = true;
try {
const res = await queryById(data.id);
// 后端返回结构直接使用 res.result 或 res depending on transform; 假设统一为 res.result
const detail = res.result || res; // 兼容不同返回包装
record.value = {
id: detail.id,
name: detail.name,
descr: detail.descr,
icon: detail.icon,
type: detail.type,
category: detail.category,
status: detail.status,
synced: !!detail.synced,
endpoint: detail.endpoint,
headers: detail.headers,
metadata: detail.metadata,
};
// 根据category解析工具
if (detail.category === 'plugin') {
// 插件类型解析tools字段为插件工具列表
let parsedPluginTools: PluginToolItem[] = [];
const rawTools = detail.tools;
if (rawTools) {
try {
if (typeof rawTools === 'string') {
parsedPluginTools = JSON.parse(rawTools);
} else if (Array.isArray(rawTools)) {
parsedPluginTools = rawTools;
}
} catch (e) {
parsedPluginTools = [];
}
}
// 确保每个工具都有enabled字段默认为true
pluginTools.value = parsedPluginTools.map((t: any) => ({
...t,
enabled: t.enabled !== undefined ? t.enabled : true
}));
tools.value = [];
} else {
// MCP类型解析tools字段为MCP工具列表
let parsedTools: McpToolItem[] = [];
const rawTools = detail.tools;
if (rawTools) {
try {
if (typeof rawTools === 'string') {
const arr = JSON.parse(rawTools);
parsedTools = arr.map((t: any) => ({
name: t.name,
descr: t.description,
enabled: t.enabled !== undefined ? t.enabled : true
}));
} else if (Array.isArray(rawTools)) {
parsedTools = rawTools.map((t: any) => ({
name: t.name,
descr: t.description,
enabled: t.enabled !== undefined ? t.enabled : true
}));
}
} catch (e) {
parsedTools = [];
}
}
tools.value = parsedTools;
pluginTools.value = [];
}
} finally {
loading.value = false;
}
});
function handleAddTool() {
openToolEditModal(true, {
pluginId: record.value?.id,
plugin: record.value,
tool: null, // 新增
});
}
function handleEditTool(tool: PluginToolItem) {
openToolEditModal(true, {
pluginId: record.value?.id,
plugin: record.value,
tool: tool,
});
}
function getMethodColor(method: string): string {
const colorMap: Record<string, string> = {
'GET': 'blue',
'POST': 'green',
'PUT': 'orange',
'DELETE': 'red',
'PATCH': 'purple',
};
return colorMap[method] || 'default';
}
async function handleToolEnabledChange(tool: PluginToolItem) {
// 更新工具启用状态
if (!record.value?.id) return;
try {
const res = await queryById(record.value.id);
const detail = res.result || res;
let tools: any[] = [];
if (detail.tools) {
try {
tools = typeof detail.tools === 'string' ? JSON.parse(detail.tools) : detail.tools;
} catch (e) {
tools = [];
}
}
const index = tools.findIndex((t: any) => t.name === tool.name);
if (index >= 0) {
tools[index].enabled = tool.enabled;
const { saveTools } = await import('../AiragMcp.api');
await saveTools(record.value.id, JSON.stringify(tools));
}
} catch (e) {
console.error('更新工具启用状态失败:', e);
// 恢复状态
tool.enabled = !tool.enabled;
}
}
function handleDeleteTool(tool: PluginToolItem) {
// 删除工具前进行二次确认
Modal.confirm({
title: '确认删除',
content: `确定要删除工具"${tool.name}"吗?此操作不可恢复。`,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
if (!record.value?.id) {
createMessage.error('插件ID不存在');
return;
}
try {
// 获取最新的工具列表
const res = await queryById(record.value.id);
const detail = res.result || res;
let tools: any[] = [];
if (detail.tools) {
try {
tools = typeof detail.tools === 'string' ? JSON.parse(detail.tools) : detail.tools;
} catch (e) {
tools = [];
}
}
// 从工具列表中移除该工具
const filteredTools = tools.filter((t: any) => t.name !== tool.name);
if (filteredTools.length === tools.length) {
createMessage.warning('未找到要删除的工具');
return;
}
// 保存更新后的工具列表
const { saveTools } = await import('../AiragMcp.api');
const saveRes = await saveTools(record.value.id, JSON.stringify(filteredTools));
if (saveRes.success) {
createMessage.success('删除成功');
// 更新前端显示
pluginTools.value = pluginTools.value.filter((t: any) => t.name !== tool.name);
// 触发成功事件,通知父组件刷新
emit('success');
} else {
createMessage.error(saveRes.message || '删除失败');
}
} catch (e) {
console.error('删除工具失败:', e);
createMessage.error('删除工具失败');
}
},
});
}
function emitEdit() {
// 触发编辑事件传递record给父组件
if (record.value) {
emit('edit', record.value);
closeModal();
}
}
function handleToolEditSuccess() {
// 重新加载详情数据
if (record.value?.id) {
loading.value = true;
queryById(record.value.id).then((res: any) => {
const detail = res.result || res;
// 更新record信息
record.value = {
id: detail.id,
name: detail.name,
descr: detail.descr,
icon: detail.icon,
type: detail.type,
category: detail.category,
status: detail.status,
synced: !!detail.synced,
endpoint: detail.endpoint,
headers: detail.headers,
metadata: detail.metadata,
};
// 根据category解析工具
if (detail.category === 'plugin') {
// 插件类型解析tools字段为插件工具列表
let parsedPluginTools: PluginToolItem[] = [];
const rawTools = detail.tools;
if (rawTools) {
try {
if (typeof rawTools === 'string') {
parsedPluginTools = JSON.parse(rawTools);
} else if (Array.isArray(rawTools)) {
parsedPluginTools = rawTools;
}
} catch (e) {
parsedPluginTools = [];
}
}
pluginTools.value = parsedPluginTools.map((t: any) => ({
...t,
enabled: t.enabled !== undefined ? t.enabled : true
}));
tools.value = [];
} else {
// MCP类型解析tools字段为MCP工具列表
let parsedTools: McpToolItem[] = [];
const rawTools = detail.tools;
if (rawTools) {
try {
if (typeof rawTools === 'string') {
const arr = JSON.parse(rawTools);
parsedTools = arr.map((t: any) => ({ name: t.name, descr: t.description }));
} else if (Array.isArray(rawTools)) {
parsedTools = rawTools.map((t: any) => ({ name: t.name, descr: t.description }));
}
} catch (e) {
parsedTools = [];
}
}
tools.value = parsedTools;
pluginTools.value = [];
}
loading.value = false;
// 触发success事件通知列表页刷新
emit('success');
}).catch(() => {
loading.value = false;
});
}
}
</script>
<style scoped lang="less">
.detail-modal {
padding: 12px 16px 8px 16px;
max-height: 520px;
overflow-y: auto;
.detail-header {
display: flex;
align-items: center;
.detail-icon {
width: 56px;
height: 56px;
border-radius: 8px;
margin-right: 16px;
background: #f5f6f7;
object-fit: cover;
}
.detail-titles {
flex: 1;
min-width: 0;
.detail-name {
font-size: 20px;
font-weight: 600;
color: #1f2329;
line-height: 28px;
max-width: 480px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.detail-type-status { margin-top: 4px; }
}
}
.detail-descr {
margin-top: 12px;
font-size: 13px;
line-height: 20px;
color: #4e5969;
background: #f5f6f7;
padding: 8px 12px;
border-radius: 6px;
max-height: 80px;
overflow-y: auto;
}
.tools-wrapper {
margin-top: 16px;
.tools-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.tools-title {
font-size: 16px;
font-weight: 600;
}
.tools-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
max-height: 280px;
overflow-y: auto;
padding-right: 4px;
}
.tool-item {
border: 1px solid #e5e6eb;
border-radius: 8px;
background: #fff;
padding: 10px 12px;
min-height: 88px;
display: flex;
flex-direction: column;
transition: box-shadow 0.25s;
cursor: default;
&:hover { box-shadow: 0 4px 10px rgba(0,0,0,0.08); }
&.tool-item-plugin {
cursor: pointer;
}
.tool-header-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
.tool-actions {
display: flex;
align-items: center;
gap: 8px;
.ant-btn-text {
padding: 0;
height: 24px;
width: 24px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: rgba(255, 77, 79, 0.1);
}
}
}
}
.tool-name {
font-size: 14px;
font-weight: 600;
color: #1d2129;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.tool-descr {
font-size: 12px;
color: #4e5969;
line-height: 18px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 8px;
}
.tool-meta {
display: flex;
align-items: center;
gap: 8px;
margin-top: auto;
.tool-path {
font-size: 12px;
color: #86909c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
.detail-modal-title {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding-right: 40px;
.detail-edit-btn {
padding: 4px 8px;
&:hover {
color: #1890ff;
background-color: rgba(24, 144, 255, 0.1);
}
}
}
</style>
<style lang="less">
.airag-mcp-detail-modal {
.jeecg-basic-modal-close > span { margin-left: 0 !important; }
:deep(.ant-modal-header) {
padding: 16px 24px;
}
:deep(.ant-modal-title) {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,477 @@
<template>
<BasicModal
destroyOnClose
@register="registerModal"
:canFullscreen="false"
width="1400px"
:title="isEdit ? '编辑工具' : '新增工具'"
wrapClassName="plugin-tool-edit-modal"
>
<div class="modal">
<div class="tool-edit-content">
<!-- 基本信息 -->
<div class="section">
<h3 class="section-title">基本信息</h3>
<a-form :model="toolForm" layout="vertical">
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="工具名称" required>
<a-input v-model:value="toolForm.name" placeholder="请输入工具名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="请求方式" required>
<a-select v-model:value="toolForm.method" placeholder="请选择请求方式">
<a-select-option value="GET">GET</a-select-option>
<a-select-option value="POST">POST</a-select-option>
<a-select-option value="PUT">PUT</a-select-option>
<a-select-option value="DELETE">DELETE</a-select-option>
<a-select-option value="PATCH">PATCH</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="24">
<a-form-item label="访问路径" required>
<a-input v-model:value="toolForm.path" placeholder="请输入访问路径,如:/api/user/{userId}" @blur="normalizePath" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="24">
<a-form-item label="描述" required>
<a-textarea v-model:value="toolForm.description" :rows="3" placeholder="请输入工具描述" />
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<!-- 请求参数 -->
<div class="section">
<div class="section-header">
<h3 class="section-title">请求参数</h3>
<a-button type="primary" size="small" @click="handleAddRequestParam">添加参数</a-button>
</div>
<a-table
:dataSource="requestParams"
:columns="requestParamsColumns"
:pagination="false"
bordered
size="small"
rowKey="tempId"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'name'">
<a-input
v-model:value="record.name"
placeholder="参数名称(字母数字下划线)"
@blur="validateParamName(record)"
/>
</template>
<template v-else-if="column.key === 'description'">
<a-input v-model:value="record.description" placeholder="参数描述" />
</template>
<template v-else-if="column.key === 'type'">
<a-select v-model:value="record.type" style="width: 100%">
<a-select-option value="String">String</a-select-option>
<a-select-option value="Number">Number</a-select-option>
<a-select-option value="Integer">Integer</a-select-option>
<a-select-option value="Boolean">Boolean</a-select-option>
</a-select>
</template>
<template v-else-if="column.key === 'location'">
<a-select v-model:value="record.location" style="width: 100%" @change="onLocationChange(record)">
<a-select-option value="Body">Raw(json)</a-select-option>
<a-select-option value="Form-Data">Form-Data</a-select-option>
<a-select-option value="Query">Query</a-select-option>
<a-select-option value="Header">Header</a-select-option>
<a-select-option value="Path">Path</a-select-option>
</a-select>
</template>
<template v-else-if="column.key === 'required'">
<a-checkbox v-model:checked="record.required" />
</template>
<template v-else-if="column.key === 'defaultValue'">
<a-input v-model:value="record.defaultValue" placeholder="默认值" />
</template>
<template v-else-if="column.key === 'action'">
<a-button type="link" danger size="small" @click="handleDeleteRequestParam(index)">删除</a-button>
</template>
</template>
</a-table>
</div>
<!-- 输出参数 -->
<div class="section">
<div class="section-header">
<h3 class="section-title">输出参数</h3>
<a-button type="primary" size="small" @click="handleAddResponseParam">添加参数</a-button>
</div>
<a-table
:dataSource="responseParams"
:columns="responseParamsColumns"
:pagination="false"
bordered
size="small"
rowKey="tempId"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'name'">
<a-input
v-model:value="record.name"
placeholder="参数名称支持点号和数组语法如data.name、data[].name"
@blur="validateResponseParamName(record)"
/>
</template>
<template v-else-if="column.key === 'description'">
<a-input v-model:value="record.description" placeholder="参数描述" />
</template>
<template v-else-if="column.key === 'type'">
<a-select v-model:value="record.type" style="width: 100%" @change="onResponseTypeChange(record)">
<a-select-option value="String">String</a-select-option>
<a-select-option value="Number">Number</a-select-option>
<a-select-option value="Integer">Integer</a-select-option>
<a-select-option value="Boolean">Boolean</a-select-option>
<a-select-option value="Object">Object</a-select-option>
<a-select-option value="Array">Array</a-select-option>
</a-select>
</template>
<template v-else-if="column.key === 'action'">
<a-button type="link" danger size="small" @click="handleDeleteResponseParam(index)">删除</a-button>
</template>
</template>
</a-table>
</div>
</div>
</div>
<template #footer>
<a-button @click="close">关闭</a-button>
<a-button type="primary" @click="handleSave" :loading="submitLoading">保存</a-button>
</template>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { useModalInner } from '/@/components/Modal';
import BasicModal from '/@/components/Modal/src/BasicModal.vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { queryById, saveTools } from '../AiragMcp.api';
const emit = defineEmits(['success', 'register']);
const { createMessage } = useMessage();
const submitLoading = ref(false);
const pluginId = ref<string | undefined>(undefined);
const isEdit = ref(false);
const toolIndex = ref<number>(-1); // 编辑时工具在数组中的索引
// 工具表单
const toolForm = reactive({
name: '',
description: '',
path: '',
method: 'GET',
});
// 请求参数列表
const requestParams = ref<any[]>([]);
// 输出参数列表
const responseParams = ref<any[]>([]);
// 请求参数表格列
const requestParamsColumns = [
{ title: '参数名称', key: 'name', width: 150 },
{ title: '参数描述', key: 'description', width: 200 },
{ title: '参数类型', key: 'type', width: 120 },
{ title: '传入方式', key: 'location', width: 150 },
{ title: '是否必须', key: 'required', width: 100 },
{ title: '默认值', key: 'defaultValue', width: 150 },
{ title: '操作', key: 'action', width: 100 },
];
// 输出参数表格列
const responseParamsColumns = [
{ title: '参数名称', key: 'name', width: 200 },
{ title: '参数描述', key: 'description', width: 200 },
{ title: '参数类型', key: 'type', width: 150 },
{ title: '操作', key: 'action', width: 100 },
];
let tempIdCounter = 0;
const [registerModal, { closeModal }] = useModalInner(async (data) => {
pluginId.value = data?.pluginId;
isEdit.value = !!data?.tool;
// 重置表单
toolForm.name = '';
toolForm.description = '';
toolForm.path = '';
toolForm.method = 'GET';
requestParams.value = [];
responseParams.value = [];
toolIndex.value = -1;
if (data?.tool) {
// 编辑模式:填充数据
toolForm.name = data.tool.name || '';
toolForm.description = data.tool.description || '';
toolForm.path = data.tool.path || '';
toolForm.method = data.tool.method || 'GET';
requestParams.value = (data.tool.parameters || []).map((p: any) => ({
...p,
tempId: `req_${tempIdCounter++}`,
required: p.required !== undefined ? p.required : false,
}));
responseParams.value = (data.tool.responses || []).map((r: any) => ({
...r,
tempId: `resp_${tempIdCounter++}`,
}));
// 查找工具在列表中的索引
if (pluginId.value) {
const res = await queryById(pluginId.value);
const detail = res.result || res;
if (detail.tools) {
try {
const tools = typeof detail.tools === 'string' ? JSON.parse(detail.tools) : detail.tools;
toolIndex.value = tools.findIndex((t: any) => t.name === data.tool.name);
} catch (e) {
toolIndex.value = -1;
}
}
}
}
});
function validateParamName(record: any) {
// 验证参数名称:只允许字母数字下划线
if (record.name && !/^[a-zA-Z0-9_]+$/.test(record.name)) {
createMessage.warning('参数名称只能包含字母、数字和下划线');
record.name = record.name.replace(/[^a-zA-Z0-9_]/g, '');
}
}
function validateResponseParamName(record: any) {
// 验证输出参数名称:支持字母、数字、下划线、点、中括号
// 只有Object和Array类型可以使用点号和中括号
if (!record.name) return;
const type = record.type;
const name = record.name;
if (type === 'Object' || type === 'Array') {
// Object和Array类型支持字母、数字、下划线、点、中括号
// 例如data.name, data[String], data[].name
if (!/^[a-zA-Z0-9_.\[\]]+$/.test(name)) {
createMessage.warning('参数名称只能包含字母、数字、下划线、点和中括号');
record.name = name.replace(/[^a-zA-Z0-9_.\[\]]/g, '');
}
} else {
// 其他类型:只允许字母数字下划线
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
createMessage.warning('非Object/Array类型参数名称只能包含字母、数字和下划线');
record.name = name.replace(/[^a-zA-Z0-9_]/g, '');
}
}
}
function onResponseTypeChange(record: any) {
// 当类型改变时如果类型不是Object或Array清除点号和中括号
if (record.type !== 'Object' && record.type !== 'Array') {
if (record.name && /[.\[\]]/.test(record.name)) {
createMessage.warning('非Object/Array类型不支持使用点号和中括号');
record.name = record.name.replace(/[.\[\]]/g, '');
}
}
}
function onLocationChange(record: any) {
// Body和Form-Data可以同时存在不再警告
// 同时存在时后端默认使用Body
}
function handleAddRequestParam() {
requestParams.value.push({
tempId: `req_${tempIdCounter++}`,
name: '',
description: '',
type: 'String',
location: 'Body',
required: false,
defaultValue: '',
});
}
function handleDeleteRequestParam(index: number) {
requestParams.value.splice(index, 1);
}
function handleAddResponseParam() {
responseParams.value.push({
tempId: `resp_${tempIdCounter++}`,
name: '',
description: '',
type: 'String',
});
}
function handleDeleteResponseParam(index: number) {
responseParams.value.splice(index, 1);
}
async function handleSave() {
try {
submitLoading.value = true;
// 验证基本字段
if (!toolForm.name || !toolForm.description || !toolForm.path || !toolForm.method) {
createMessage.warning('请填写完整的工具基本信息');
return;
}
// Body和Form-Data可以同时存在后端会默认使用Body
if (!pluginId.value) {
createMessage.error('插件ID不存在');
return;
}
// 加载插件数据
const res = await queryById(pluginId.value);
const detail = res.result || res;
// 解析现有工具列表
let tools: any[] = [];
if (detail.tools) {
try {
const parsedTools = typeof detail.tools === 'string' ? JSON.parse(detail.tools) : detail.tools;
// 确保是数组
if (Array.isArray(parsedTools)) {
tools = [...parsedTools]; // 复制数组,避免引用问题
} else {
tools = [];
}
} catch (e) {
console.error('解析工具列表失败:', e);
tools = [];
}
}
// 确保tools是数组
if (!Array.isArray(tools)) {
tools = [];
}
// 构建当前工具数据移除tempId
const parameters = requestParams.value.map(p => {
const { tempId: _tempId, ...param } = p;
return param;
});
const responses = responseParams.value.map(r => {
const { tempId: _tempId, ...resp } = r;
return resp;
});
const toolData = {
name: toolForm.name,
description: toolForm.description,
path: toolForm.path,
method: toolForm.method,
enabled: true, // 默认启用
parameters,
responses,
};
// 根据编辑状态处理工具
if (isEdit.value && toolIndex.value >= 0 && toolIndex.value < tools.length) {
// 编辑模式:更新现有工具
tools[toolIndex.value] = toolData;
} else {
// 新增模式:添加新工具
// 检查工具名称是否已存在
const nameExists = tools.some((t: any) => t.name === toolForm.name);
if (nameExists) {
createMessage.error('工具名称已存在,请使用不同的名称');
return;
}
tools.push(toolData);
}
// 构建工具列表JSON字符串
const toolsJson = JSON.stringify(tools);
// 调用保存工具接口
const saveRes = await saveTools(pluginId.value, toolsJson);
if (saveRes.success) {
createMessage.success('保存成功');
emit('success');
closeModal();
} else {
createMessage.error(saveRes.message || '保存失败');
}
} finally {
submitLoading.value = false;
}
}
function close() {
closeModal();
}
// 规范化路径:确保以/开头
function normalizePath() {
if (toolForm.path && !toolForm.path.startsWith('/')) {
toolForm.path = '/' + toolForm.path;
}
}
</script>
<style scoped lang="less">
.modal {
padding: 16px;
.tool-edit-content {
.section {
margin-bottom: 24px;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.section-title {
margin: 0;
font-size: 16px;
font-weight: bold;
}
:deep(.ant-table) {
.ant-table-thead > tr > th {
background: #fafafa;
padding: 8px;
}
.ant-table-tbody > tr > td {
padding: 8px;
}
}
}
}
}
</style>
<style lang="less">
.plugin-tool-edit-modal {
.jeecg-basic-modal-close > span { margin-left: 0 !important; }
.ant-modal {
max-height: 90vh;
.ant-modal-body {
max-height: calc(90vh - 110px);
overflow-y: auto;
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -44,20 +44,20 @@
</div>
<div class="mt-6">
<ul>
<li class="flex mr-14">
<span class="label">模型类型</span>
<span class="described">{{ item.modelType_dictText }}</span>
<li class="flex mr-14" style="align-items: center;">
<span class="label" style="flex-shrink: 0;">模型类型</span>
<span class="described" style="flex: 1; min-width: 0;">{{ item.modelType_dictText }}</span>
<a-tooltip v-if="!item.activateFlag" title="未激活模型暂无法被系统其他功能调用,激活后可正常使用。">
<span class="no-activate">未激活</span>
</a-tooltip>
</li>
<li class="flex mr-14 mt-6">
<span class="label">基础模型</span>
<span class="described">{{ item.modelName }}</span>
<li class="flex mr-14 mt-6" style="align-items: center;">
<span class="label" style="flex-shrink: 0;">基础模型</span>
<span class="described" style="flex: 1; min-width: 0;">{{ item.modelName }}</span>
</li>
<li class="flex mr-14 mt-6">
<span class="label">创建者</span>
<span class="described">{{ item.createBy_dictText || item.createBy }}</span>
<li class="flex mr-14 mt-6" style="align-items: center;">
<span class="label" style="flex-shrink: 0;">创建者</span>
<span class="described" style="flex: 1; min-width: 0;">{{ item.createBy_dictText || item.createBy }}</span>
</li>
</ul>
</div>
@ -336,6 +336,7 @@
white-space: nowrap;
overflow: hidden;
font-size: 12px;
flex: 1;
}
.flex {

View File

@ -98,8 +98,9 @@
</div>
<template v-if="dataIndex === 'add' || dataIndex === 'edit'" #footer>
<a-button @click="cancel">关闭</a-button>
<a-button @click="test" v-if="modelActivate" :loading="testLoading" type="default">测试连接</a-button>
<a-button @click="save" type="primary" ghost="true">保存</a-button>
<a-button @click="test" v-if="!modelActivate" :loading="testLoading" type="primary" >保存并激活</a-button>
<a-button @click="test(false)" v-if="!modelActivate" :loading="testLoading" type="primary" >保存并激活</a-button>
</template>
<template v-else #footer> </template>
</BasicModal>
@ -118,6 +119,7 @@
import { formSchema, imageList } from '../model.data';
import { editModel, queryById, saveModel, testConn } from '../model.api';
import { useMessage } from '/@/hooks/web/useMessage';
const {createMessage: $message, createConfirm} = useMessage();
import AiModelSeniorForm from './AiModelSeniorForm.vue';
import { cloneDeep } from "lodash-es";
export default {
@ -234,6 +236,7 @@
initModelProvider();
dataIndex.value = 'list';
modelNameAddOption.value = [];
modelActivate.value = false;
}
});
@ -349,7 +352,7 @@
/**
* 测试连接
*/
async function test() {
async function test(onlyTest = false) {
try {
testLoading.value = true;
let values = await validate();
@ -368,9 +371,13 @@
values.provider = modelData.value.value;
}
//测试
await testConn(values).then((result) => {
await testConn(values).then(async (result) => {
if(onlyTest){
$message.success('测试连接成功');
return true;
}
modelActivate.value = true;
save();
await save();
});
} catch (e) {
if (e.hasOwnProperty('errorFields')) {

View File

@ -1,5 +1,22 @@
{
"data": [
{
"title": "Anthropic",
"value": "ANTHROPIC",
"LLM": [
{"label": "claude-sonnet-4-20250514", "value": "claude-sonnet-4-20250514","descr": "【Claude 4系列】Claude Sonnet 4具有卓越推理能力的高性能模型。\n\n支持文本和图像输入文本输出拥有200k上下文窗口1M上下文测试版可用。","type": "text,image"},
{"label": "claude-opus-4-20250514", "value": "claude-opus-4-20250514","descr": "【Claude 4系列】Claude Opus 4是最强大、最有能力的模型。\n\n支持文本和图像输入文本输出拥有200k上下文窗口卓越的推理能力。","type": "text,image"},
{"label": "claude-3-7-sonnet-20250219", "value": "claude-3-7-sonnet-20250219","descr": "【Claude 3.7系列】Claude Sonnet 3.7中型模型,具备卓越的推理能力和效率。\n\n适合企业工作负载和大规模AI部署。","type": "text,image"},
{"label": "claude-3-5-sonnet-20241022", "value": "claude-3-5-sonnet-20241022","descr": "Claude 3.5 Sonnet是Anthropic最强大的AI模型。\n\n在编程、多步骤工作流、图表解释等复杂任务中表现出色。支持200k上下文长度支持8k最大输出。","type": "text,image"},
{"label": "claude-3-5-haiku-20241022", "value": "claude-3-5-haiku-20241022","descr": "【快速模型】Claude 3.5 Haiku是Anthropic最快的AI模型。\n\n响应速度快成本较低适合高频交互场景。支持200k上下文长度支持8k最大输出。","type": "text,image"},
{"label": "claude-3-opus-20240229", "value": "claude-3-opus-20240229","descr": "Claude 3 Opus是Claude 3系列中性能最强的模型。\n\n在高度复杂的任务上表现出色如编写高质量代码、数学推理等。支持200k上下文长度支持4k最大输出。","type": "text,image"},
{"label": "claude-3-sonnet-20240229", "value": "claude-3-sonnet-20240229","descr": "Claude 3 Sonnet在智能和速度之间取得了良好平衡。\n\n适合企业工作负载和大规模AI部署。支持200k上下文长度支持4k最大输出。","type": "text,image"},
{"label": "claude-3-haiku-20240307", "value": "claude-3-haiku-20240307","descr": "Claude 3 Haiku是Claude 3系列中最快的模型。\n\n提供接近即时的响应适合无缝AI体验。支持200k上下文长度支持4k最大输出。","type": "text,image"}
],
"type": ["LLM"],
"baseUrl": "https://api.anthropic.com/v1",
"LLMDefaultValue": "claude-3-5-sonnet-20241022"
},
{
"title": "DeepSeek",
"value": "DEEPSEEK",

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -1,11 +1,12 @@
import { FormSchema } from '@/components/Form';
import deepspeek from '/@/views/super/airag/aimodel/icon/deepspeek.png';
import ollama from '/@/views/super/airag/aimodel/icon/ollama.png';
import OpenAi from '/@/views/super/airag/aimodel/icon/OpenAi.png';
import qianfan from '/@/views/super/airag/aimodel/icon/qianfan.png';
import qianwen from '/@/views/super/airag/aimodel/icon/qianwen.png';
import zhipuai from '/@/views/super/airag/aimodel/icon/zhipuai.png';
import anthropic from './icon/anthropic.png';
import deepspeek from './icon/deepspeek.png';
import ollama from './icon/ollama.png';
import OpenAi from './icon/OpenAi.png';
import qianfan from './icon/qianfan.png';
import qianwen from './icon/qianwen.png';
import zhipuai from './icon/zhipuai.png';
import { ref } from 'vue';
/**
@ -63,7 +64,7 @@ export const formSchema: FormSchema[] = [
component: 'InputPassword',
ifShow: ({ values }) => {
if(values.provider==='DEEPSEEK' || values.provider==="OLLAMA" || values.provider==="OPENAI"
|| values.provider==="ZHIPU" || values.provider==="QWEN"){
|| values.provider==="ZHIPU" || values.provider==="QWEN" || values.provider==="ANTHROPIC"){
return false;
}
return true;
@ -83,6 +84,7 @@ export const formSchema: FormSchema[] = [
* @param name
*/
export const imageList = ref<any>({
ANTHROPIC: anthropic,
DEEPSEEK: deepspeek,
OLLAMA: ollama,
OPENAI: OpenAi,

View File

@ -99,7 +99,7 @@
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
import { useDesign } from '/@/hooks/web/useDesign';
import { getCodeInfo } from '/@/api/sys/user';
//import { onKeyStroke } from '@vueuse/core';
import { encryptAESCBC } from '/@/utils/cipher';
const ACol = Col;
const ARow = Row;
@ -143,9 +143,12 @@
if (!data) return;
try {
loading.value = true;
// 密码使用AES加密传输
const encryptedPassword = encryptAESCBC(data.password);
const { userInfo } = await userStore.login(
toRaw({
password: data.password,
password: encryptedPassword,
username: data.account,
captcha: data.inputCode,
checkKey: randCodeData.checkKey,
@ -166,18 +169,13 @@
duration: 3,
});
loading.value = false;
//update-begin-author:taoyan date:2022-5-3 for: issues/41 登录页面,当输入验证码错误时,验证码图片要刷新一下,而不是保持旧的验证码图片不变
handleChangeCheckCode();
//update-end-author:taoyan date:2022-5-3 for: issues/41 登录页面,当输入验证码错误时,验证码图片要刷新一下,而不是保持旧的验证码图片不变
}
}
function handleChangeCheckCode() {
formData.inputCode = '';
//TODO 兼容mock和接口暂时这样处理
//update-begin---author:chenrui ---date:2025/1/7 for[QQYUN-10775]验证码可以复用 #7674------------
// 代码逻辑说明: [QQYUN-10775]验证码可以复用 #7674------------
randCodeData.checkKey = new Date().getTime() + Math.random().toString(36).slice(-4); // 1629428467008;
//update-end---author:chenrui ---date:2025/1/7 for[QQYUN-10775]验证码可以复用 #7674------------
getCodeInfo(randCodeData.checkKey).then((res) => {
randCodeData.randCodeImage = res;
randCodeData.requestCodeSuccess = true;

View File

@ -1,5 +1,5 @@
<template>
<BasicModal v-bind="config" @register="registerModal" :title="currTitle" wrapClassName="loginSelectModal" v-model:visible="visible">
<BasicModal v-bind="config" @register="registerModal" :title="currTitle" wrapClassName="loginSelectModal" v-model:visible="visible" :maxHeight="500">
<a-form ref="formRef" :model="formState" :rules="rules" v-bind="layout" :colon="false" class="loginSelectForm">
<!--多租户选择-->
<a-form-item v-if="isMultiTenant" name="tenantId" :validate-status="validate_status">

View File

@ -84,8 +84,7 @@
}
//倒计时执行前的函数
function sendCodeApi() {
//update-begin---author:wangshuai---date:2025-07-15---for:【issues/8567】严重修改密码存在水平越权问题登录应该用登录模板不应该用忘记密码的模板---
// 代码逻辑说明: 【issues/8567】严重修改密码存在水平越权问题登录应该用登录模板不应该用忘记密码的模板---
return getCaptcha({ mobile: formData.mobile, smsmode: SmsEnum.LOGIN });
//update-end---author:wangshuai---date:2025-07-15---for:【issues/8567】严重修改密码存在水平越权问题登录应该用登录模板不应该用忘记密码的模板---
}
</script>

View File

@ -96,9 +96,8 @@
//先获取钉钉的企业id如果没有配置 还是走原来的逻辑,走原来的逻辑 需要判断存不存在token存在token直接去首页
let tenantId = getAuthCache(OAUTH2_THIRD_LOGIN_TENANT_ID) || 0;
let url = `/sys/thirdLogin/get/corpId/clientId?tenantId=${tenantId}`;
//update-begin---author:wangshuai---date:2024-12-09---for:不要使用getAction online里面的要用defHttp---
// 代码逻辑说明: 不要使用getAction online里面的要用defHttp---
defHttp.get({ url:url },{ isTransformResponse: false }).then((res) => {
//update-end---author:wangshuai---date:2024-12-09---for:不要使用getAction online里面的要用defHttp---
if (res.success) {
if(res.result && res.result.corpId && res.result.clientId){
requestAuthCode({ corpId: res.result.corpId, clientId: res.result.clientId }).then((res) => {

View File

@ -59,7 +59,7 @@
if(info){
let query = JSON.parse(info);
//update-begin-author:taoyan date:2023-4-27 for: QQYUN-4882【简流】节点消息通知 邮箱 点击办理跳到了应用首页
// 代码逻辑说明: QQYUN-4882【简流】节点消息通知 邮箱 点击办理跳到了应用首页
let path = '';
if(query.isLowApp === 1){
path = '/myapps/personalOffice/myTodo'
@ -67,7 +67,6 @@
let taskId = query.taskId;
path = '/task/handle/' + taskId
}
//update-end-author:taoyan date:2023-4-27 for: QQYUN-4882【简流】节点消息通知 邮箱 点击办理跳到了应用首页
router.replace({ path, query });
notification.success({

View File

@ -166,7 +166,6 @@ async function checkPhone(rule, value, callback) {
}
}
//update-begin---author:wangshuai ---date:20220629 for[issues/I5BG1I]vue3不支持auth2登录------------
/**
* 判断是否是OAuth2APP环境
*/
@ -189,17 +188,14 @@ export function isOAuth2DingAppEnv() {
export function sysOAuth2Login(source) {
let url = `${window._CONFIG['domianURL']}/sys/thirdLogin/oauth2/${source}/login`;
url += `?state=${encodeURIComponent(window.location.origin)}`;
//update-begin---author:wangshuai ---date:20230224 for[QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------
// 代码逻辑说明: [QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------
let tenantId = getAuthCache(OAUTH2_THIRD_LOGIN_TENANT_ID);
if(tenantId){
url += `&tenantId=${tenantId}`;
}
//update-end---author:wangshuai ---date:20230224 for[QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------
window.location.href = url;
}
//update-end---author:wangshuai ---date:20220629 for[issues/I5BG1I]vue3不支持auth2登录------------
//update-begin---author:wangshuai ---date:20241108 for[QQYUN-9421]vue3新版auth登录用户不用点击登录------------
/**
* 后台callBack
* @param code
@ -213,4 +209,3 @@ export function sysOAuth2Callback(code:string) {
}
window.location.href = url;
}
//update-end---author:wangshuai ---date:20241108 for[QQYUN-9421]vue3新版auth登录用户不用点击登录------------

View File

@ -5,7 +5,7 @@
import { unref } from 'vue';
import { useRouter } from 'vue-router';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
// update-begin--author:liaozhiyang---date:20231123---for【QQYUN-7099】动态路由匹配右键重新加载404
// 代码逻辑说明: 【QQYUN-7099】动态路由匹配右键重新加载404
const { currentRoute, replace } = useRouter();
const { params, query } = unref(currentRoute);
const { path } = params;
@ -26,5 +26,4 @@
});
}
}
// update-end--author:liaozhiyang---date:20231123---for【QQYUN-7099】动态路由匹配右键重新加载404
</script>

View File

@ -1,9 +1,9 @@
import { defHttp } from '/@/utils/http/axios';
export enum Api {
list = '/sys/user/queryByOrgCodeForAddressList',
list = '/sys/user/queryDepartUserByOrgCode',
positionList = '/sys/position/list',
queryDepartTreeSync = '/sys/sysDepart/queryDepartAndPostTreeSync',
queryDepartTreeSync = '/sys/sysDepart/queryDepartTreeSync',
}
/**
* 获取部门树列表

View File

@ -1,42 +1,61 @@
import { FormSchema } from '/@/components/Form';
import { BasicColumn } from '/@/components/Table';
import { getDepartName } from "@/utils/common/compUtils";
export const columns: BasicColumn[] = [
{
title: '姓名',
dataIndex: 'realname',
width: 150,
},
{
title: '工号',
dataIndex: 'workNo',
width: 100,
},
{
title: '部门',
dataIndex: 'orgCodeTxt',
width: 200,
dataIndex: 'departName',
customRender:({ text })=>{
return getDepartName(text);
}
},
{
title: '主岗位',
dataIndex: 'mainDepPostId_dictText',
width: 200,
dataIndex: 'postName',
customRender:({ text })=>{
return getDepartName(text);
}
},
{
title: '兼职岗位',
dataIndex: 'otherPostName',
customRender:({ text })=>{
return getDepartName(text);
}
},
/* {
title: '职务',
dataIndex: 'post',
width: 150,
slots: { customRender: 'post' },
},
},*/
{
title: '手机',
width: 150,
width: 110,
dataIndex: 'phone',
customRender:( { record, text })=>{
if(record.izHideContact && record.izHideContact === '1'){
return '/';
}
return text;
}
},
{
title: '邮箱',
width: 150,
width: 180,
dataIndex: 'email',
customRender:( { record, text })=>{
if(record.izHideContact && record.izHideContact === '1'){
return text?'/':'';
}
return text;
}
},
];
@ -47,10 +66,4 @@ export const searchFormSchema: FormSchema[] = [
component: 'Input',
colProps: { span: 6 },
},
{
label: '工号',
field: 'workNo',
component: 'Input',
colProps: { span: 6 },
},
];

View File

@ -14,9 +14,10 @@
:load-data="loadChildrenTreeData"
v-model:expandedKeys="expandedKeys"
@select="onSelect"
style="overflow-y: auto;height: calc(100vh - 330px);"
>
<template #title="{ orgCategory, title }">
<TreeIcon :orgCategory="orgCategory" :title="title"></TreeIcon>
<template #title="{ orgCategory, title, departNameAbbr }">
<TreeIcon :orgCategory="orgCategory" :title="getTitle(title,departNameAbbr)"></TreeIcon>
</template>
</a-tree>
</template>
@ -147,7 +148,7 @@
try {
loading.value = true;
treeData.value = [];
let result = await searchByKeywords({ keyWord: value, orgCategory: '1,2,3,4' });
let result = await searchByKeywords({ keyWord: value, orgCategory: '1,2,4' });
if (Array.isArray(result)) {
treeData.value = result;
}
@ -171,6 +172,18 @@
}
}
/**
* 获取标题
* @param title 部门名称
* @param departNameAbbr 缩写
*/
function getTitle(title, departNameAbbr) {
if (departNameAbbr){
return departNameAbbr;
}
return title;
}
defineExpose({
loadRootTreeData,
});

View File

@ -15,6 +15,9 @@
.join(',')
}}
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
</div>
</a-col>
@ -47,15 +50,17 @@
tableProps: {
api: list,
columns,
//update-begin---author:wangshuai ---date:20220629 for[VUEN-1485]进入系统管理--通讯录页面后,网页命令行报错------------
// 代码逻辑说明: [VUEN-1485]进入系统管理--通讯录页面后,网页命令行报错------------
rowKey: 'id',
//update-end---author:wangshuai ---date:20220629 for[VUEN-1485]进入系统管理--通讯录页面后,网页命令行报错--------------
showIndexColumn: true,
formConfig: {
schemas: searchFormSchema,
},
canResize: false,
showTableSetting: false,
actionColumn:{
width: 80
},
// 请求之前对参数做处理
beforeFetch(params) {
params.orgCode = orgCode.value;
@ -71,6 +76,20 @@
reload();
}
/**
* 操作栏
*
* @param record
*/
function getTableAction(record) {
return [
{
label: '发消息',
onClick: handleSendChat.bind(null, record),
},
];
}
// 查询职务信息
async function queryPositionInfo() {
const result = await positionList({ pageSize: 99999 });
@ -83,6 +102,24 @@
}
}
queryPositionInfo();
/**
* 聊天
*
* @param record
*/
function handleSendChat(record) {
//获取messageId
let cacheByDynKey = getCacheByDynKey(JEECG_CHAT_UID);
let iframes:any = document.getElementById("jChatOnline");
let id = record.id;
//发送打开聊天窗口的请求
iframes.contentWindow.postMessage({
type: "open-chat",
messageId: cacheByDynKey,
data: { id: id, type: "friend", groupName: "", avatar: record.avatar, username: record.realname }
}, "*");
}
</script>
<style lang="less">

View File

@ -28,6 +28,15 @@
</a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="桌面应用" name="webDownloadUrl">
<a-input placeholder="桌面应用安装路径" v-model:value="model.webDownloadUrl">
<template #addonAfter>
<Icon icon="ant-design:upload-outlined" style="cursor: pointer" @click="showUploadModal('web')" />
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="更新内容">
<a-textarea :rows="4" v-model:value="model.updateNote" placeholder="请输入更新内容" />
@ -84,6 +93,7 @@
updateNote: '',
downloadUrl: '',
wgtUrl: '',
webDownloadUrl: '',
});
/**
@ -113,6 +123,8 @@
createMessage.success('保存成功');
confirmLoading.value = false;
active.value = false;
}).finally(() => {
confirmLoading.value = false;
});
}
@ -122,7 +134,7 @@
*/
function showUploadModal(type) {
uploadType.value = type;
modalValue.value = type == 'apk' ? model.downloadUrl : model.wgtUrl;
modalValue.value = type == 'web'? model.webDownloadUrl :type == 'apk' ? model.downloadUrl : model.wgtUrl;
openModal(true, {
maxCount: 1,
bizPath: filePath,
@ -135,15 +147,17 @@
function uploadBack(value) {
if (unref(uploadType) == 'apk') {
model.downloadUrl = value;
} else {
} else if (unref(uploadType) == 'wgt') {
model.wgtUrl = value;
}else{
model.webDownloadUrl = value;
}
}
//表单校验规则
const validatorRules = {
appVersion: [{ required: true, message: '版本不能为空', trigger: 'blur' }],
downloadUrl: [{ required: true, message: 'APP安装apk不能为空', trigger: 'change' }],
wgtUrl: [{ required: true, message: 'APP热更新文件不能为空', trigger: 'change' }],
wgtUrl: [{ required: false, message: 'APP热更新文件不能为空', trigger: 'change' }],
};
// 显示字段
const schema: DescItem[] = [
@ -159,6 +173,10 @@
field: 'wgtUrl',
label: 'APP热更新文件',
},
{
field: 'webDownloadUrl',
label: '桌面应用下载地址',
},
{
field: 'updateNote',
label: '更新内容',

View File

@ -41,9 +41,8 @@ export const formSchema: FormSchema[] = [
field: 'pid',
component: 'TreeSelect',
componentProps: {
//update-begin---author:wangshuai ---date:20230829 forreplaceFields已过期使用fieldNames代替------------
// 代码逻辑说明: replaceFields已过期使用fieldNames代替------------
fieldNames: {
//update-end---author:wangshuai ---date:20230829 forreplaceFields已过期使用fieldNames代替------------
value: 'key',
},
dropdownStyle: {

View File

@ -35,9 +35,8 @@
expandedRowKeys.value = [];
setModalProps({ confirmLoading: false, minHeight: 80 });
isUpdate.value = !!data?.isUpdate;
//update-begin---author:wangshuai ---date: 20230829 for分类字典data.record为空报错------------
// 代码逻辑说明: 分类字典data.record为空报错------------
isSubAdd.value = !data?.isUpdate && data.record && data.record.id;
//update-end---author:wangshuai ---date: 20230829 for分类字典data.record为空报错------------
if (data?.record) {
//表单赋值
await setFieldsValue({

View File

@ -130,9 +130,8 @@
* 导入
*/
function importSuccess() {
//update-begin---author:wangshuai ---date:20220530 for[issues/54]树字典,勾选,然后批量删除,系统错误------------
// 代码逻辑说明: [issues/54]树字典,勾选,然后批量删除,系统错误------------
(selectedRowKeys.value = []) && reload();
//update-end---author:wangshuai ---date:20220530 for[issues/54]树字典,勾选,然后批量删除,系统错误--------------
}
/**
* 添加下级
@ -156,14 +155,11 @@
reload();
} else {
//新增子集
//update-begin-author:liusq---date:20230411--for: [issue/4550]分类字典数据量过多会造成数据查询时间过长---
// 代码逻辑说明: [issue/4550]分类字典数据量过多会造成数据查询时间过长---
if(isSubAdd){
await expandTreeNode(values.pid);
//update-end-author:liusq---date:20230411--for: [issue/4550]分类字典数据量过多会造成数据查询时间过长---
}else{
//update-begin-author:wangshuai---date:20240319--for: 字典树删除之后其他节点出现loading---
//expandedRowKeys.value = [];
//update-end-author:wangshuai---date:20240319--for: 字典树删除之后其他节点出现loading---
for (let key of unref(expandedArr)) {
await expandTreeNode(key);
}
@ -257,11 +253,10 @@
* */
async function expandTreeNode(key) {
let record:any = findTableDataRecord(key);
//update-begin-author:liusq---date:20230411--for: [issue/4550]分类字典数据量过多会造成数据查询时间过长,显示“接口请求超时,请刷新页面重试!”---
// 代码逻辑说明: [issue/4550]分类字典数据量过多会造成数据查询时间过长,显示“接口请求超时,请刷新页面重试!”---
if(!expandedRowKeys.value.includes(key)){
expandedRowKeys.value.push(key);
}
//update-end-author:liusq---date:20230411--for: [issue/4550]分类字典数据量过多会造成数据查询时间过长,显示“接口请求超时,请刷新页面重试!”---
let result = await getChildList({ pid: key });
if (result && result.length > 0) {
record.children = getDataByResult(result);

View File

@ -159,7 +159,6 @@
}
</script>
<style lang="less">
// update-begin-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
@prefix-cls: ~'@{namespace}-j-depart-form-content';
/*begin 兼容暗夜模式*/
@ -168,7 +167,6 @@
border-top: 1px solid @border-color-base;
}
/*end 兼容暗夜模式*/
// update-end-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
</style>
<style lang="less" scoped>
:deep(.ant-select-selector .ant-select-selection-item){

View File

@ -56,6 +56,9 @@
v-model:expandedKeys="expandedKeys"
@check="onCheck"
@select="onSelect"
draggable
@drop="onDrop"
@dragstart="onDragStart"
style="overflow-y: auto;height: calc(100vh - 330px);"
>
<template #title="{ key: treeKey, title, dataRef, data }">
@ -106,14 +109,14 @@
</template>
<script lang="ts" setup>
import { inject, nextTick, ref, unref, defineEmits } from 'vue';
import { inject, nextTick, ref, unref, defineEmits, h } from 'vue';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { useMethods } from '/@/hooks/system/useMethods';
import { Api, deleteBatchDepart, queryDepartAndPostTreeSync } from '../depart.api';
import { Api, deleteBatchDepart, queryDepartAndPostTreeSync, updateChangeDepart } from '../depart.api';
import { searchByKeywords } from '/@/views/system/departUser/depart.user.api';
import DepartFormModal from '/@/views/system/depart/components/DepartFormModal.vue';
import { Popconfirm } from 'ant-design-vue';
import { Modal, Popconfirm } from 'ant-design-vue';
import TreeIcon from "@/components/Form/src/jeecg/components/TreeIcon/TreeIcon.vue";
const prefixCls = inject('prefixCls');
@ -354,15 +357,109 @@
}
function onExportXls() {
//update-begin---author:wangshuai---date:2024-07-05---for:【TV360X-1671】部门管理不支持选中的记录导出---
// 代码逻辑说明: 【TV360X-1671】部门管理不支持选中的记录导出---
let params = {}
if(checkedKeys.value && checkedKeys.value.length > 0) {
params['selections'] = checkedKeys.value.join(',')
}
handleExportXls('部门信息', Api.exportXlsUrl,params);
//update-end---author:wangshuai---date:2024-07-05---for:【TV360X-1671】部门管理不支持选中的记录导出---
}
/**
* 拖拽开始时,只关闭被拖拽的当前节点
*
* @param info
*/
function onDragStart(info: any) {
const dragKey = info.node?.key;
if (!dragKey){
return;
}
// 只关闭被拖拽的当前节点,不关闭其子节点
if (expandedKeys.value.includes(dragKey)) {
expandedKeys.value = expandedKeys.value.filter(key => key !== dragKey);
}
}
/**
* 拖拽结束
* @param info
*/
function onDrop (info){
const dropKey = info.node.key;
const dragKey = info.dragNode.key;
const dropPos = info.node.pos.split('-');
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
const dropTitle = info.node.title;
const dragTitle = info.dragNode.title;
//禁止拖拽到子节点
if (isDescendant(info.dragNode, info.node.key)) {
createMessage.warning('不能拖拽到自身后代');
return;
}
if(dropKey === dragKey){
createMessage.warning('不能自身拖拽到自身');
return;
}
let pos = "中";
if(dropPosition === -1){
pos = "上方";
}else if (dropPosition === 1){
pos = "下方";
}
let text = "将【" + dragTitle + "】移动到【" + dropTitle + "】" + pos + "";
Modal.confirm({
title: '确认移动',
content: h('div', {}, [
h('p', { style: { marginBottom: '12px', fontSize: '14px' } }, text),
h('p', {
style: {
color: '#ff4d4f',
fontSize: '13px',
margin: '0'
}
}, '移动后:机构编码会改变,历史业务数据保留原机构编码,此操作不可撤销!')
]),
okText: '确认',
cancelText: '取消',
onOk: () => {
updateChangeDepart({ dragId: dragKey, dropId: dropKey, dropPosition: dropPosition, sort: info.dropPosition }).then(res=>{
if(res.success){
createMessage.success('部门顺序调整成功');
//重新加载树
treeData.value = [];
selectedKeys.value = [];
loadRootTreeData();
} else {
createMessage.error(res.message);
}
}).catch(e=>{
createMessage.error(e.message);
})
}
})
}
/**
* 判断目标节点是否在拖拽节点的子树中(避免循环引用)
*
* @param dragNode
* @param targetKey
*/
function isDescendant(dragNode, targetKey) {
const stack = [...(dragNode.children ?? [])];
while (stack.length) {
const node = stack.pop()!;
if (node.key === targetKey){
return true;
}
if (node.children){
stack.push(...node.children);
}
}
return false;
}
defineExpose({
loadRootTreeData,
});

View File

@ -1,7 +1,18 @@
<template>
<a-spin :spinning="loading">
<template v-if="treeData && treeData.length > 0">
<BasicTree ref="basicTree" :treeData="treeData" :checkStrictly="true" style="height: 500px; overflow: auto"></BasicTree>
<div style="margin-top: 10px;margin-bottom: 10px;display: flex">
<a-button preIcon="ant-design:down-outlined" @click="expandAll(true)" type="primary">展开全部</a-button>
<a-button preIcon="ant-design:up-outlined" @click="expandAll(false)" type="primary" style="margin-left: 10px">折叠全部</a-button>
</div>
<BasicTree
:expandedKeys="expandedKeys"
:fieldNames="{ children: 'children', title: 'title', key: 'value' }"
ref="basicTree"
:treeData="treeData"
:checkStrictly="true"
style="height: 500px; overflow: auto"
></BasicTree>
</template>
<a-empty v-else description="无岗位消息" />
</a-spin>
@ -22,6 +33,10 @@
const loading = ref<boolean>(false);
//树的全部节点信息
const treeData = ref<any[]>([]);
//选中的key
const expandedKeys = ref<any[]>([]);
//所有的部门id
const departIds = ref<any[]>([]);
watch(departId, (val) => loadData(val), { immediate: true });
@ -31,12 +46,43 @@
await getRankRelation({ departId: val }).then((res) => {
if (res.success) {
treeData.value = res.result;
departIds.value = getParentDepartmentIds(res.result);
}
});
} finally {
loading.value = false;
}
}
/**
* 折叠全部
*
* @param expandAll
*/
async function expandAll(expandAll) {
if (!expandAll) {
expandedKeys.value = [];
} else {
expandedKeys.value = departIds.value;
}
}
/**
* 获取存在子级的部门id
* @param departments
*/
function getParentDepartmentIds(departments) {
const ids: any = [];
departments.forEach((dept) => {
// 检查是否有 children 数组且不为空
if (dept.children && Array.isArray(dept.children) && dept.children.length > 0) {
ids.push(dept.id);
// 递归检查子部门是否也有子级
ids.push(...getParentDepartmentIds(dept.children));
}
});
return ids;
}
</script>
<style lang="less" scoped>

View File

@ -84,7 +84,7 @@
// onCreated
loadData({
success: (ids) => {
// update-begin--author:liaozhiyang---date:20240704---for【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
// 代码逻辑说明: 【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
const localData = localStorage.getItem(DEPART_MANGE_AUTH_CONFIG_KEY);
if (localData) {
const obj = JSON.parse(localData);
@ -93,7 +93,6 @@
} else {
// expandedKeys.value = ids;
}
// update-end--author:liaozhiyang---date:20240704---for【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
}
});
watch(departId, () => loadDepartPermission(), { immediate: true });
@ -102,13 +101,11 @@
try {
loading.value = true;
let { treeList, ids } = await queryRoleTreeList();
//update-begin---author:wangshuai---date:2024-04-08---for:【issues/1169】部门管理功能中的【部门权限】中未翻译 t('') 多语言---
// 代码逻辑说明: 【issues/1169】部门管理功能中的【部门权限】中未翻译 t('') 多语言---
treeData.value = translateTitle(treeList);
//update-end---author:wangshuai---date:2024-04-08---for:【issues/1169】部门管理功能中的【部门权限】中未翻译 t('') 多语言---
// update-begin--author:liaozhiyang---date:20240704---for【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
// 代码逻辑说明: 【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
allTreeKeys.value = ids;
options.success?.(ids);
// update-end--author:liaozhiyang---date:20240704---for【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
} finally {
loading.value = false;
}
@ -219,7 +216,7 @@
// 切换展开收起
async function toggleExpandAll(flag) {
// update-begin--author:liaozhiyang---date:20240704---for【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
// 代码逻辑说明: 【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
if (flag) {
expandedKeys.value = allTreeKeys.value;
saveLocalOperation('expand', 'openAll');
@ -227,30 +224,27 @@
expandedKeys.value = [];
saveLocalOperation('expand', 'closeAll');
}
// update-end--author:liaozhiyang---date:20240704---for【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
}
// 切换全选
async function toggleCheckALL(flag) {
// update-begin--author:liaozhiyang---date:20240704---for【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
// 代码逻辑说明: 【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
if (flag) {
checkedKeys.value = allTreeKeys.value;
} else {
checkedKeys.value = [];
}
// update-end--author:liaozhiyang---date:20240704---for【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
}
// 切换层级关联(独立)
const toggleRelationAll = (flag) => {
// update-begin--author:liaozhiyang---date:20240704---for【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
// 代码逻辑说明: 【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
checkStrictly.value = flag;
if (flag) {
saveLocalOperation('level', 'standAlone');
} else {
saveLocalOperation('level', 'relation');
}
// update-end--author:liaozhiyang---date:20240704---for【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
};
/**
* 2024-07-04

View File

@ -26,7 +26,7 @@
import UserDrawer from '/@/views/system/user/UserDrawer.vue';
import UserSelectModal from '/@/components/Form/src/jeecg/components/modal/UserSelectModal.vue';
import { queryByOrgCodeForAddressList } from '../depart.api';
import { queryDepartPostByOrgCode } from '../depart.api';
import { ColEx } from '/@/components/Form/src/types';
import { userColumns } from '@/views/system/depart/depart.data';
import { linkDepartUserBatch } from '@/views/system/departUser/depart.user.api';
@ -53,7 +53,7 @@
// 列表页面公共参数、方法
const { tableContext, createMessage } = useListPage({
tableProps: {
api: queryByOrgCodeForAddressList,
api: queryDepartPostByOrgCode,
columns: userColumns,
canResize: false,
rowKey: 'id',
@ -130,6 +130,7 @@
// 查看用户详情
function showUserDetail(record) {
record.activitiSync = record.activitiSync? Number(record.activitiSync) : 1;
openDrawer(true, {
record,
isUpdate: true,
@ -139,6 +140,7 @@
// 编辑用户信息
function editUserInfo(record) {
record.activitiSync = record.activitiSync? Number(record.activitiSync) : 1;
openDrawer(true, { isUpdate: true, record, departDisabled: true, departPostDisabled: true });
}

View File

@ -0,0 +1,44 @@
<!-- 部门负责人页面 -->
<template>
<!--引用表格-->
<BasicTable @register="registerTable"></BasicTable>
</template>
<script setup lang="ts" name="DepartmentHeadList">
import { BasicTable } from '@/components/Table';
import { useListPage } from '@/hooks/system/useListPage';
import { userColumns } from '@/views/system/depart/depart.data';
import { getDepartmentHead } from '../depart.api';
import { computed, watch } from 'vue';
const props = defineProps({
data: { require: true, type: Object },
});
// 当前部门id
const departId = computed(() => props.data?.id);
// 列表页面公共参数、方法
const { tableContext } = useListPage({
tableProps: {
api: getDepartmentHead,
columns: userColumns,
canResize: false,
rowKey: 'id',
tableSetting: { cacheKey: 'depart_head_list' },
// 请求之前对参数做处理
beforeFetch(params) {
return Object.assign(params, { departId: departId.value });
},
showActionColumn: false,
immediate: !!departId.value,
},
});
// 注册 ListTable
const [registerTable, { reload }] = tableContext;
watch(
() => props.data,
() => reload()
);
</script>
<style scoped lang="less"></style>

View File

@ -31,7 +31,11 @@ export enum Api {
//异步获取部门和岗位
queryDepartAndPostTreeSync = '/sys/sysDepart/queryDepartAndPostTreeSync',
//获取部门和岗位下的成员
queryByOrgCodeForAddressList = '/sys/user/queryByOrgCodeForAddressList',
queryDepartPostByOrgCode = '/sys/user/queryDepartPostByOrgCode',
//更新拖拽部门后的位置
updateChangeDepart = '/sys/sysDepart/updateChangeDepart',
//获取负责部门
getDepartmentHead = '/sys/sysDepart/getDepartmentHead',
}
/**
@ -150,4 +154,18 @@ export const getRankRelation = (params) => defHttp.get({ url: Api.getRankRelatio
*
* @param params
*/
export const queryByOrgCodeForAddressList = (params) => defHttp.get({ url: Api.queryByOrgCodeForAddressList, params });
export const queryDepartPostByOrgCode = (params) => defHttp.get({ url: Api.queryDepartPostByOrgCode, params });
/**
* 更新拖拽部门后的位置
*
* @param params
*/
export const updateChangeDepart = (params) => defHttp.put({ url: Api.updateChangeDepart, params },{ isTransformResponse: false });
/**
* 获取负责部门
*
* @param params
*/
export const getDepartmentHead = (params) => defHttp.get({ url: Api.getDepartmentHead, params });

View File

@ -2,7 +2,12 @@ import { FormSchema } from '/@/components/Form';
import { getPositionByDepartId } from "./depart.api";
import { useMessage } from "@/hooks/web/useMessage";
import { BasicColumn } from "@/components/Table";
import { getDepartPathNameByOrgCode } from '@/utils/common/compUtils';
import {
getDepartName,
getDepartPathName,
getDepartPathNameByOrgCode,
getMultiDepartPathName
} from '@/utils/common/compUtils';
import { h, ref } from 'vue';
const { createMessage: $message } = useMessage();
@ -21,6 +26,14 @@ export function useBasicFormSchema(treeData) {
},
rules: [{ required: true, message: '机构名称不能为空' }],
},
{
field: 'departNameAbbr',
label: '机构简称',
component: 'Input',
componentProps: {
placeholder: '请输入机构/部门简称',
}
},
{
field: 'parentId',
label: '上级部门',
@ -172,11 +185,6 @@ export const orgCategoryOptions = {
* 用户列表
*/
export const userColumns: BasicColumn[] = [
{
title: '用户账号',
dataIndex: 'username',
width: 120,
},
{
title: '姓名',
dataIndex: 'realname',
@ -186,10 +194,33 @@ export const userColumns: BasicColumn[] = [
title: '手机',
width: 150,
dataIndex: 'phone',
customRender:( { record, text })=>{
if(record.izHideContact && record.izHideContact === '1'){
return '/';
}
return text;
}
},
{
title: '主岗位',
dataIndex: 'mainDepPostId_dictText',
dataIndex: 'mainDepPostId',
customRender: ({ record, text })=>{
if(!text){
return '';
}
return getDepartName(getDepartPathName(record.mainDepPostId_dictText,text,false));
},
width: 200,
},
{
title: '兼职岗位',
dataIndex: 'otherDepPostId',
customRender: ({ record, text })=>{
if(!text){
return '';
}
return getDepartName(getMultiDepartPathName(record.otherDepPostId_dictText,text));
},
width: 200,
},
];

View File

@ -26,6 +26,9 @@
<DepartUserList :data="departData" :key="reRender"></DepartUserList>
</div>
</a-tab-pane>
<a-tab-pane tab="部门负责人" key="departmentHead">
<DepartmentHeadList :data="departData"></DepartmentHeadList>
</a-tab-pane>
</a-tabs>
<div v-show="departData == null" style="padding-top: 40px">
<a-empty description="尚未选择部门" />
@ -43,6 +46,7 @@
import DepartRuleTab from './components/DepartRuleTab.vue';
import DepartRankRelation from './components/DepartRankRelation.vue';
import DepartUserList from './components/DepartUserList.vue';
import DepartmentHeadList from './components/DepartmentHeadList.vue';
const { prefixCls } = useDesign('depart-manage');
provide('prefixCls', prefixCls);

View File

@ -92,7 +92,7 @@
departId.value = data.record.departId;
loadData({
success: (ids) => {
// update-begin--author:liaozhiyang---date:20240704---for【TV360X-1619】同步系统角色改法加上缓存默认层级关联修正原生层级关联bug
// 代码逻辑说明: 【TV360X-1619】同步系统角色改法加上缓存默认层级关联修正原生层级关联bug
const localData = localStorage.getItem(DEPART_ROLE_AUTH_CONFIG_KEY);
if (localData) {
const obj = JSON.parse(localData);
@ -101,7 +101,6 @@
} else {
// expandedKeys.value = ids;
}
// update-end--author:liaozhiyang---date:20240704---for【TV360X-1619】同步系统角色改法加上缓存默认层级关联修正原生层级关联bug
},
});
});
@ -115,12 +114,10 @@
const { ids, treeList } = await queryTreeListForDeptRole({ departId: departId.value });
if (ids.length > 0) {
allTreeKeys.value = ids;
// update-begin--author:liaozhiyang---date:20240704---for【TV360X-1619】同步系统角色改法加上缓存默认层级关联修正原生层级关联bug
// 代码逻辑说明: 【TV360X-1619】同步系统角色改法加上缓存默认层级关联修正原生层级关联bug
options.success?.(ids);
// update-end--author:liaozhiyang---date:20240704---for【TV360X-1619】同步系统角色改法加上缓存默认层级关联修正原生层级关联bug
//update-begin---author:wangshuai---date:2024-04-08---for:【issues/1169】我的部门功能中的【部门权限】中未翻译 t('') 多语言---
// 代码逻辑说明: 【issues/1169】我的部门功能中的【部门权限】中未翻译 t('') 多语言---
treeData.value = translateTitle(treeList);
//update-end---author:wangshuai---date:2024-04-08---for:【issues/1169】我的部门功能中的【部门权限】中未翻译 t('') 多语言---
// 查询角色授权
checkedKeys.value = await queryDeptRolePermission({ roleId: roleId.value });
lastCheckedKeys.value = [checkedKeys.value];

View File

@ -97,9 +97,8 @@
beforeFetch(params) {
params.deptId = departId.value;
},
// update-begin--author:liaozhiyang---date:20240517---for【TV360X-53】未选择部门的情况下部门角色全查出来了
// 代码逻辑说明: 【TV360X-53】未选择部门的情况下部门角色全查出来了
immediate: !!departId.value,
// update-end--author:liaozhiyang---date:20240517---for【TV360X-53】未选择部门的情况下部门角色全查出来了
},
});
@ -117,9 +116,7 @@
() => reload()
);
onMounted(() => {
// update-begin--author:liaozhiyang---date:20240517---for【TV360X-53】未选择部门的情况下部门角色全查出来了
// reload();
// update-end--author:liaozhiyang---date:20240517---for【TV360X-53】未选择部门的情况下部门角色全查出来了
});
// 清空选择的行

View File

@ -96,9 +96,8 @@
beforeFetch(params) {
params.depId = departId.value;
},
// update-begin--author:liaozhiyang---date:20240717---for【TV360X-1861】没部门时不加载用户信息
// 代码逻辑说明: 【TV360X-1861】没部门时不加载用户信息
immediate: !!departId.value,
// update-end--author:liaozhiyang---date:20240717---for【TV360X-1861】没部门时不加载用户信息
},
});
@ -161,9 +160,8 @@
// 选择添加已有用户
function selectAddUser() {
// update-begin--author:liaozhiyang---date:20240308---for【TV360X-1613】再次打开还是上次的选中用户没置空
// 代码逻辑说明: 【TV360X-1613】再次打开还是上次的选中用户没置空
userSelectModalRef.value.rowSelection.selectedRowKeys = [];
// update-end--author:liaozhiyang---date:20240308---for【TV360X-1613】再次打开还是上次的选中用户没置空
selUserModal.openModal();
}

View File

@ -42,12 +42,11 @@
// 左侧树选择后触发
function onTreeSelect(data) {
// update-begin--author:liaozhiyang---date:20250106---for【issues/7658】我的部门无部门列表数据时点击查询或者重置能查出数据
// 代码逻辑说明: 【issues/7658】我的部门无部门列表数据时点击查询或者重置能查出数据
if (reRender.value == -1) {
// 重新渲染组件
reRender.value = Math.random();
}
// update-end--author:liaozhiyang---date:20250106---for【issues/7658】我的部门无部门列表数据时点击查询或者重置能查出数据
departData.value = data;
}
</script>

View File

@ -1,6 +1,5 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit" width="800px">
<!-- update-begin---author:wangshuai---date:2023-10-23---for:QQYUN-6804后台模式字典没有颜色配置--- -->
<BasicForm @register="registerForm" >
<template #itemColor="{ model, field }">
<div class="item-tool">
@ -16,7 +15,6 @@
</div>
</template>
</BasicForm>
<!-- update-end---author:wangshuai---date:2023-10-23---for:QQYUN-6804后台模式字典没有颜色配置--- -->
</BasicModal>
</template>
<script lang="ts" setup>

View File

@ -63,7 +63,6 @@
fixed: undefined,
},
});
// update-begin--author:liaozhiyang---date:20240709---for【TV360X-1663】数据字典回收增加批量功能
/**
* 选择列配置
*/
@ -79,7 +78,6 @@
function onSelectChange(selectedRowKeys: (string | number)[]) {
checkedKeys.value = selectedRowKeys;
}
// update-end--author:liaozhiyang---date:20240709---for【TV360X-1663】数据字典回收增加批量功能
/**
* 还原事件
*/
@ -98,11 +96,10 @@
*/
function batchHandleRevert() {
batchPutRecycleBin({ ids: toRaw(checkedKeys.value).join(',') }, () => {
// update-begin--author:liaozhiyang---date:20240709---for【TV360X-1663】数据字典回收增加批量功能
// 代码逻辑说明: 【TV360X-1663】数据字典回收增加批量功能
reload();
checkedKeys.value = [];
emit('success');
// update-end--author:liaozhiyang---date:20240709---for【TV360X-1663】数据字典回收增加批量功能
});
}
/**
@ -110,10 +107,9 @@
*/
function batchHandleDelete() {
batchDeleteRecycleBin({ ids: toRaw(checkedKeys.value).join(',') }, () => {
// update-begin--author:liaozhiyang---date:20240709---for【TV360X-1663】数据字典回收增加批量功能
// 代码逻辑说明: 【TV360X-1663】数据字典回收增加批量功能
checkedKeys.value = [];
reload();
// update-end--author:liaozhiyang---date:20240709---for【TV360X-1663】数据字典回收增加批量功能
});
}
//获取操作栏事件

View File

@ -77,7 +77,7 @@
width: 240,
},
},
//update-begin---author:wangshuai ---date:20220616 for[issues/I5AMDD]导入/导出功能,操作后提示没有传递 export.url/import.url 参数------------
// 代码逻辑说明: [issues/I5AMDD]导入/导出功能,操作后提示没有传递 export.url/import.url 参数------------
exportConfig: {
name: '数据字典列表',
url: getExportUrl,
@ -85,7 +85,6 @@
importConfig: {
url: getImportUrl,
},
//update-end---author:wangshuai ---date:20220616 for[issues/I5AMDD]导入/导出功能,操作后提示没有传递 export.url/import.url 参数--------------
});
//注册table数据
@ -128,11 +127,10 @@
*/
async function batchHandleDelete() {
await batchDeleteDict({ ids: selectedRowKeys.value }, () => {
// update-begin--author:liaozhiyang---date:20240701---for【TV360X-1665】数据字典批量删除后选中也清空
// 代码逻辑说明: 【TV360X-1665】数据字典批量删除后选中也清空
reload();
selectedRowKeys.value = [];
selectedRows.value = [];
// update-end--author:liaozhiyang---date:20240701---for【TV360X-1665】数据字典批量删除后选中也清空
});
}
/**
@ -153,10 +151,9 @@
if (result.success) {
const res = await queryAllDictItems();
removeAuthCache(DB_DICT_DATA_KEY);
// update-begin--author:liaozhiyang---date:20230908---for【QQYUN-6417】生产环境字典慢的问题
// 代码逻辑说明: 【QQYUN-6417】生产环境字典慢的问题
const userStore = useUserStore();
userStore.setAllDictItems(res.result);
// update-end--author:liaozhiyang---date:20230908---for【QQYUN-6417】生产环境字典慢的问题
createMessage.success('刷新缓存完成!');
} else {
createMessage.error('刷新缓存失败!');

View File

@ -241,13 +241,12 @@
createMessage.warn(t('sys.login.mobilePlaceholder'));
return;
}
//update-begin---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
// 代码逻辑说明: 【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
const result = await getCaptcha({ mobile: formData.mobile, smsmode: SmsEnum.FORGET_PASSWORD }).catch((res) =>{
if(res.code === ExceptionEnum.PHONE_SMS_FAIL_CODE){
openCaptchaModal(true, {});
}
});
//update-end---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
if (result) {
const TIME_COUNT = 60;
if (!unref(timer)) {

View File

@ -31,7 +31,7 @@
>{{ t('sys.login.mobileSignInFormTitle') }}
</div>
</div>
<div class="aui-form-box" style="height: 180px">
<div class="aui-form-box" style="height: 240px">
<a-form ref="loginRef" :model="formData" v-if="activeIndex === 'accountLogin'" @keyup.enter.native="loginHandleClick">
<div class="aui-account">
<div class="aui-inputClear">
@ -56,11 +56,23 @@
<img v-else style="margin-top: 2px; max-width: initial" :src="codeImg" @click="handleChangeCheckCode" />
</div>
</div>
<div class="aui-inputClear" v-if="showDepart">
<i class="icon icon-depart"></i>
<div class="JLoginSelectDept">
<a-select allow-clear style="width: 100%" :bordered="false" v-model:value="formData.loginOrgCode" :placeholder="t('sys.login.loginOrgCode')">
<template #suffixIcon>
<Icon icon="ant-design:gold-outline" />
</template>
<template v-for="depart in departList" :key="depart.orgCode">
<a-select-option :value="depart.orgCode">{{ getShortDeptName(depart.label) }}</a-select-option>
</template>
</a-select>
</div>
</div>
<div class="aui-flex">
<div class="aui-flex-box">
<div class="aui-choice">
<a-input class="fix-auto-fill" type="checkbox" v-model:value="rememberMe" />
<span style="margin-left: 5px">{{ t('sys.login.rememberMe') }}</span>
<a-checkbox v-model:checked="rememberMe">{{ t('sys.login.rememberMe') }}</a-checkbox>
</div>
</div>
<div class="aui-forget">
@ -83,6 +95,18 @@
<span class="aui-get-code code-shape">{{ t('component.countdown.sendText', [unref(timeRuning)]) }}</span>
</div>
</div>
<div class="aui-inputClear" v-if="showDepart">
<div class="JLoginSelectDept">
<a-select allow-clear style="width: 100%" :bordered="false" v-model:value="phoneFormData.loginOrgCode" :placeholder="t('sys.login.loginOrgCode')">
<template #suffixIcon>
<Icon icon="ant-design:gold-outline" />
</template>
<template v-for="depart in departList" :key="depart.orgCode">
<a-select-option :value="depart.orgCode">{{ getShortDeptName(depart.label) }}</a-select-option>
</template>
</a-select>
</div>
</div>
</div>
</a-form>
</div>
@ -144,14 +168,14 @@
</div>
<!-- 第三方登录相关弹框 -->
<ThirdModal ref="thirdModalRef"></ThirdModal>
<!-- 图片验证码弹窗 -->
<CaptchaModal @register="captchaRegisterModal" @ok="getLoginCode" />
</div>
</template>
<script lang="ts" setup name="login-mini">
import { getCaptcha, getCodeInfo } from '/@/api/sys/user';
import { computed, onMounted, reactive, ref, toRaw, unref } from 'vue';
import { computed, onMounted, reactive, ref, toRaw, unref, watch } from 'vue';
import codeImg from '/@/assets/images/checkcode.png';
import { Rule } from '/@/components/Form';
import { useUserStore } from '/@/store/modules/user';
@ -166,12 +190,15 @@
import adTextImg from '/@/assets/loginmini/icon/jeecg_ad_text.png';
import { AppLocalePicker, AppDarkModeToggle } from '/@/components/Application';
import { useLocaleStore } from '/@/store/modules/locale';
import { createLocalStorage } from '/@/utils/cache';
import { useDesign } from "/@/hooks/web/useDesign";
import { useAppInject } from "/@/hooks/web/useAppInject";
import { GithubFilled, WechatFilled, DingtalkCircleFilled, createFromIconfontCN } from '@ant-design/icons-vue';
import CaptchaModal from '@/components/jeecg/captcha/CaptchaModal.vue';
import { useModal } from "@/components/Modal";
import { ExceptionEnum } from "@/enums/exceptionEnum";
import { encryptAESCBC } from '/@/utils/cipher';
import { defHttp } from "@/utils/http/axios";
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_2316098_umqusozousr.js',
@ -180,6 +207,7 @@
const { notification, createMessage } = useMessage();
const userStore = useUserStore();
const { t } = useI18n();
const $ls = createLocalStorage();
const localeStore = useLocaleStore();
const showLocale = localeStore.getShowPicker;
const randCodeData = reactive<any>({
@ -187,7 +215,9 @@
requestCodeSuccess: false,
checkKey: null,
});
const rememberMe = ref<string>('0');
// 记住用户名
const rememberMe = ref<boolean>(false);
const REMEMBER_USERNAME_KEY = 'LOGIN_REMEMBER_USERNAME';
//手机号登录还是账号登录
const activeIndex = ref<string>('accountLogin');
const type = ref<string>('login');
@ -196,11 +226,13 @@
inputCode: '',
username: 'admin',
password: '123456',
loginOrgCode: '',
});
//手机登录表单字段
const phoneFormData = reactive<any>({
mobile: '',
smscode: '',
loginOrgCode: '',
});
const loginRef = ref();
//第三方登录弹窗
@ -225,15 +257,104 @@
type: Boolean,
},
});
//**********************查询部门逻辑begin**********************************************
//用户部门
const departList = ref([]);
//部门显示
const showDepart = computed(()=>{
return departList.value.length > 1
})
//获取部门缩写
const getShortDeptName = computed(()=>{
return (deptName) => {
if (!deptName) return '';
if (deptName.length > 18) {
return '...' + deptName.substring(deptName.length-18, deptName.length) ;
}
return deptName;
};
})
//监听验证码和输入框的修改
watch(
() => [formData.inputCode, phoneFormData.smscode],
() => {
if ((formData.inputCode && formData.inputCode.length == 4)
|| (phoneFormData.smscode && phoneFormData.smscode.length == 6)) {
checkAccount()
}
},
);
/**
* 监听账号变化,清除部门信息
*/
watch(
() => [formData.username,phoneFormData.mobile,activeIndex.value],
() => {
formData.loginOrgCode = null;
phoneFormData.loginOrgCode = null;
departList.value = [];
if ((formData.inputCode && formData.inputCode.length == 4)
|| (phoneFormData.smscode && phoneFormData.smscode.length == 6)) {
checkAccount()
}
}
);
//初始化数据
let deptTimer;
function checkAccount() {
deptTimer && clearTimeout(deptTimer);
deptTimer = setTimeout(async () => {
let loginType = activeIndex.value === 'accountLogin' ? 'account' : 'phone';
// 验证条件提取
const isValidAccount = loginType === 'account' && formData.username && formData.password;
const isValidPhone = loginType == 'phone' && phoneFormData.mobile && phoneFormData.smscode;
let finalFormData = loginType == 'phone' ? {...phoneFormData} : {...formData};
if (!isValidAccount && !isValidPhone) {
return;
}
//查询部门信息前,优先进行账户校验
if (departList.value && departList.value.length == 0) {
let params = {...finalFormData, loginType: activeIndex.value === 'accountLogin' ? 'account' : 'phone'};
if (loginType == 'account') {
params['password'] = encryptAESCBC(formData.password);
params['checkKey'] = randCodeData.checkKey;
}
const res = await defHttp.post({
url: '/sys/loginGetUserDeparts',
params: {...params}
}, {isTransformResponse: false});
if (res.success && res.result) {
let {departs,currentOrgCode} = res.result;
// 判断当前部门是否在所属的部门列表中
if (departs && departs.length > 0) {
// 代码逻辑说明: JHHB-790 用户部门变更,会出现这个情况(因为之前设置的这里只切换部门,过滤了公司和岗位信息)
const hasCurrentDepart = departs.some(item => item.orgCode == currentOrgCode);
formData.loginOrgCode = hasCurrentDepart?currentOrgCode:null;
phoneFormData.loginOrgCode = hasCurrentDepart?currentOrgCode:null;
departList.value = departs.map((item) => {
return {
label: item.departName,
value: item.orgCode,
orgCode: item.orgCode,
departName: item.departName,
};
});
}
} else {
//createMessage.warn(res.message);
}
}
},500)
}
//**********************查询部门逻辑end*************************************************
/**
* 获取验证码
*/
function handleChangeCheckCode() {
formData.inputCode = '';
//update-begin---author:chenrui ---date:2025/1/7 for[QQYUN-10775]验证码可以复用 #7674------------
// 代码逻辑说明: [QQYUN-10775]验证码可以复用 #7674------------
randCodeData.checkKey = new Date().getTime() + Math.random().toString(36).slice(-4); // 1629428467008;
//update-end---author:chenrui ---date:2025/1/7 for[QQYUN-10775]验证码可以复用 #7674------------
getCodeInfo(randCodeData.checkKey).then((res) => {
randCodeData.randCodeImage = res;
randCodeData.requestCodeSuccess = true;
@ -270,10 +391,14 @@
}
try {
loginLoading.value = true;
// 密码使用AES加密传输
const encryptedPassword = encryptAESCBC(formData.password);
const { userInfo } = await userStore.login(
toRaw({
password: formData.password,
password: encryptedPassword,
username: formData.username,
loginOrgCode: formData.loginOrgCode,
captcha: formData.inputCode,
checkKey: randCodeData.checkKey,
mode: 'none', //不要默认的错误提示
@ -285,6 +410,12 @@
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realname}`,
duration: 3,
});
// 登录成功后处理记住用户名
if (rememberMe.value && formData.username) {
$ls.set(REMEMBER_USERNAME_KEY, formData.username)
} else {
$ls.remove(REMEMBER_USERNAME_KEY)
}
}
} catch (error) {
notification.error({
@ -315,6 +446,7 @@
const { userInfo }: any = await userStore.phoneLogin({
mobile: phoneFormData.mobile,
captcha: phoneFormData.smscode,
loginOrgCode: phoneFormData.loginOrgCode,
mode: 'none', //不要默认的错误提示
});
if (userInfo) {
@ -343,15 +475,12 @@
createMessage.warn(t('sys.login.mobilePlaceholder'));
return;
}
//update-begin---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
//update-begin---author:wangshuai---date:2025-07-15---for:【issues/8567】严重修改密码存在水平越权问题登录应该用登录模板不应该用忘记密码的模板---
// 代码逻辑说明: 【issues/8567】严重修改密码存在水平越权问题登录应该用登录模板不应该用忘记密码的模板---
const result = await getCaptcha({ mobile: phoneFormData.mobile, smsmode: SmsEnum.LOGIN }).catch((res) =>{
//update-end---author:wangshuai---date:2025-07-15---for:【issues/8567】严重修改密码存在水平越权问题登录应该用登录模板不应该用忘记密码的模板---
if(res.code === ExceptionEnum.PHONE_SMS_FAIL_CODE){
openCaptchaModal(true, {});
}
});
//update-end---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
if (result) {
const TIME_COUNT = 60;
if (!unref(timer)) {
@ -431,6 +560,12 @@
onMounted(() => {
//加载验证码
handleChangeCheckCode();
// 恢复已记住的用户名
const saved = $ls.get(REMEMBER_USERNAME_KEY);
if (saved) {
formData.username = saved;
rememberMe.value = true;
}
});
</script>
@ -487,6 +622,13 @@
.top-3{
top: 0.45rem;
}
.JLoginSelectDept {
margin:5px auto;
:deep(.ant-select-selection-placeholder) {
font-size: 14px;
color: #9a9a9a;
}
}
</style>
<style lang="less">
@ -544,7 +686,7 @@ html[data-theme='dark'] {
.ant-checkbox-inner,.aui-success h3{
border-color: #c9d1d9;
}
//update-begin---author:wangshuai ---date:20230828 for【QQYUN-6363】这个样式代码有问题不在里面导致表达式有问题------------
// 代码逻辑说明: 【QQYUN-6363】这个样式代码有问题不在里面导致表达式有问题------------
&-sign-in-way {
.anticon {
font-size: 22px !important;
@ -556,7 +698,6 @@ html[data-theme='dark'] {
}
}
}
//update-end---author:wangshuai ---date:20230828 for【QQYUN-6363】这个样式代码有问题不在里面导致表达式有问题------------
}
input.fix-auto-fill,
@ -564,7 +705,7 @@ html[data-theme='dark'] {
-webkit-text-fill-color: #c9d1d9 !important;
box-shadow: inherit !important;
}
.ant-divider-inner-text {
font-size: 12px !important;
color: @text-color-secondary !important;

View File

@ -139,13 +139,12 @@
createMessage.warn(t('sys.login.mobilePlaceholder'));
return;
}
//update-begin---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
// 代码逻辑说明: 【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
const result = await getCaptcha({ mobile: formData.mobile, smsmode: SmsEnum.REGISTER }).catch((res) =>{
if(res.code === ExceptionEnum.PHONE_SMS_FAIL_CODE){
openCaptchaModal(true, {});
}
});
//update-end---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
if (result) {
const TIME_COUNT = 60;
if (!unref(timer)) {

View File

@ -96,9 +96,8 @@
//先获取钉钉的企业id如果没有配置 还是走原来的逻辑,走原来的逻辑 需要判断存不存在token存在token直接去首页
let tenantId = getAuthCache(OAUTH2_THIRD_LOGIN_TENANT_ID) || 0;
let url = `/sys/thirdLogin/get/corpId/clientId?tenantId=${tenantId}`;
//update-begin---author:wangshuai---date:2024-12-09---for:不要使用getAction online里面的要用defHttp---
// 代码逻辑说明: 不要使用getAction online里面的要用defHttp---
defHttp.get({ url:url },{ isTransformResponse: false }).then((res) => {
//update-end---author:wangshuai---date:2024-12-09---for:不要使用getAction online里面的要用defHttp---
if (res.success) {
if(res.result && res.result.corpId && res.result.clientId){
requestAuthCode({ corpId: res.result.corpId, clientId: res.result.clientId }).then((res) => {

View File

@ -42,9 +42,8 @@
updateSchema([
{
field: 'parentId',
// update-begin--author:liaozhiyang---date:20240306---for【QQYUN-8379】菜单管理页菜单国际化
// 代码逻辑说明: 【QQYUN-8379】菜单管理页菜单国际化
componentProps: { treeData: translateMenu(treeData, 'name') },
// update-end--author:liaozhiyang---date:20240306---for【QQYUN-8379】菜单管理页菜单国际化
},
{
field: 'name',
@ -108,13 +107,12 @@
placeholder = '请输入组件名称';
}
updateSchema([{ field: 'componentName', componentProps: { placeholder } }]);
//update-begin---author:wangshuai ---date:20230204 for[QQYUN-4058]菜单添加智能化处理------------
// 代码逻辑说明: [QQYUN-4058]菜单添加智能化处理------------
if (httpUrl != null && httpUrl != '') {
if (httpUrl.startsWith('http://') || httpUrl.startsWith('https://')) {
setFieldsValue({ component: httpUrl });
}
}
//update-end---author:wangshuai ---date:20230204 for[QQYUN-4058]菜单添加智能化处理------------
}
/**

View File

@ -51,7 +51,6 @@
columns[0].customRender = function ({ text, record }) {
// date-begin--author:liaozhiyang---date:20250716---for【issues/8317】默认首页菜单名称适配国际化报错
let displayText = text;
// update-begin--author:liaozhiyang---date:20240306---for【QQYUN-8379】菜单管理页菜单国际化
// 先处理国际化,避免在添加默认首页标记后影响国际化检查
if (displayText && displayText.includes("t('") && t) {
try {
@ -62,7 +61,6 @@
displayText = text;
}
}
// update-end--author:liaozhiyang---date:20240306---for【QQYUN-8379】菜单管理页菜单国际化
// 在国际化处理完成后,再添加默认首页标记
const isDefIndex = checkDefIndex(record);
if (isDefIndex) {
@ -88,10 +86,9 @@
showIndexColumn: false,
tableSetting: { fullScreen: true },
formConfig: {
// update-begin--author:liaozhiyang---date:20230803---for【QQYUN-5873】查询区域lablel默认居左
// 代码逻辑说明: 【QQYUN-5873】查询区域lablel默认居左
labelWidth: 74,
rowProps: { gutter: 24 },
// update-end--author:liaozhiyang---date:20230803---for【QQYUN-5873】查询区域lablel默认居左
schemas: searchFormSchema,
autoAdvancedCol: 4,
baseColProps: { xs: 24, sm: 12, md: 6, lg: 6, xl: 6, xxl: 6 },

View File

@ -101,11 +101,10 @@ export const formSchema: FormSchema[] = [
required: !isButton(e),
},
]);
//update-begin---author:wangshuai ---date:20220729 for[VUEN-1834]只有一级菜单,才默认值,子菜单的时候,清空------------
// 代码逻辑说明: [VUEN-1834]只有一级菜单,才默认值,子菜单的时候,清空------------
if (isMenu(e) && !formModel.id && (formModel.component=='layouts/default/index' || formModel.component=='layouts/RouteView')) {
formModel.component = '';
}
//update-end---author:wangshuai ---date:20220729 for[VUEN-1834]只有一级菜单,才默认值,子菜单的时候,清空------------
},
};
},
@ -122,13 +121,12 @@ export const formSchema: FormSchema[] = [
component: 'TreeSelect',
required: true,
componentProps: {
//update-begin---author:wangshuai ---date:20230829 forreplaceFields已过期使用fieldNames代替------------
// 代码逻辑说明: replaceFields已过期使用fieldNames代替------------
fieldNames: {
label: 'name',
key: 'id',
value: 'id',
},
//update-end---author:wangshuai ---date:20230829 forreplaceFields已过期使用fieldNames代替------------
dropdownStyle: {
maxHeight: '50vh',
},
@ -141,14 +139,12 @@ export const formSchema: FormSchema[] = [
label: '访问路径',
component: 'Input',
required: true,
//update-begin-author:liusq date:2023-06-06 for: [issues/5008]子表数据权限设置不生效
// 代码逻辑说明: [issues/5008]子表数据权限设置不生效
ifShow: ({ values }) => !(values.component === ComponentTypes.IFrame && values.internalOrExternal),
//update-begin-author:zyf date:2022-11-02 for: 聚合路由允许路径重复
// 代码逻辑说明: 聚合路由允许路径重复
dynamicRules: ({ model, schema,values }) => {
return checkPermDuplication(model, schema, values.menuType !== 2?true:false);
},
//update-end-author:zyf date:2022-11-02 for: 聚合路由允许路径重复
//update-end-author:liusq date:2022-06-06 for: [issues/5008]子表数据权限设置不生效
},
{
field: 'component',
@ -402,7 +398,7 @@ export const dataRuleFormSchema: FormSchema[] = [
getPopupContainer: (node) => document.body,
},
},
// update-begin--author:liaozhiyang---date:20240724---for【TV360X-1864】添加系统变量
// 代码逻辑说明: 【TV360X-1864】添加系统变量
{
field: 'ruleValue',
component: 'JInputSelect',
@ -445,7 +441,6 @@ export const dataRuleFormSchema: FormSchema[] = [
],
},
},
// update-end--author:liaozhiyang---date:20240724---for【TV360X-1864】添加系统变量
{
field: 'status',
label: '状态',

View File

@ -18,10 +18,9 @@
<template #renderItem="{ item }">
<a-list-item :style="{ background: item?.izTop && item.izTop == 1 ? '#f7f7f7' : 'auto' }">
<template #actions>
<a-rate :value="item.starFlag=='1'?1:0" :count="1" @click="clickStar(item)" style="cursor: pointer" disabled />
<a-rate class="antd-rate" :value="item.starFlag=='1'?1:0" :count="1" @click="clickStar(item)" style="cursor: pointer" disabled />
</template>
<!-- update-begin-author:taoyan date:2023-5-10 for: QQYUN-4744系统通知6系统通知@人后对方看不到是哪个表单@没有超链接 -->
<a-list-item-meta>
<template #description>
<div v-if="isFormComment(item)" style="background: #f7f7f7;color: #555;padding: 2px 5px;white-space:nowrap;overflow: hidden">
@ -31,7 +30,6 @@
{{item.createTime}}
</div>
</template>
<!-- update-end-author:taoyan date:2023-5-10 for: QQYUN-4744系统通知6系统通知@人后对方看不到是哪个表单@没有超链接 -->
<template #title>
<div style="position: relative">
@ -75,6 +73,20 @@
<a-avatar v-else style="background: #79919d"><alert-outlined style="font-size: 16px"/></a-avatar>
</template>
<template v-else-if="['eoa_co_remind', 'eoa_co_notify'].includes(item.busType)">
<a-badge dot v-if="noRead(item)" class="msg-no-read">
<a-avatar style="background: #79919d"><GatewayOutlined style="font-size: 16px" title="未读消息"/></a-avatar>
</a-badge>
<a-avatar v-else style="background: #79919d"><GatewayOutlined style="font-size: 16px"/></a-avatar>
</template>
<template v-else-if="['eoa_sup_remind', 'eoa_sup_notify'].includes(item.busType)">
<a-badge dot v-if="noRead(item)" class="msg-no-read">
<a-avatar style="background: #79919d"><AlertOutlined style="font-size: 16px" title="未读消息"/></a-avatar>
</a-badge>
<a-avatar v-else style="background: #79919d"><AlertOutlined style="font-size: 16px"/></a-avatar>
</template>
<template v-else>
<a-badge dot v-if="noRead(item)" class="msg-no-read">
<a-avatar style="background: #79919d"><bell-filled style="font-size: 16px" title="未读消息"/></a-avatar>
@ -87,16 +99,14 @@
</template>
</a-list>
<!-- update-begin-author:liusq date:2023-10-26 for: [QQYUN-6713]系统通知打开弹窗修改 -->
<keep-alive>
<component v-if="currentModal" v-bind="bindParams" :key="currentModal" :is="currentModal" @register="modalRegCache[currentModal].register" />
</keep-alive>
<!-- update-end-author:liusq date:2023-10-26 for: [QQYUN-6713]系统通知打开弹窗修改 -->
</template>
<script>
import { FilterOutlined, CloseOutlined, BellFilled, ExclamationOutlined, MailOutlined,InteractionOutlined, AlertOutlined } from '@ant-design/icons-vue';
import { FilterOutlined, CloseOutlined, BellFilled, ExclamationOutlined, MailOutlined,InteractionOutlined, AlertOutlined, GatewayOutlined } from '@ant-design/icons-vue';
import { useSysMessage, useMessageHref } from './useSysMessage';
import {getGloablEmojiIndex, useEmojiHtml} from "/@/components/jeecg/comment/useComment";
import { ref, h, watch } from "vue";
@ -111,6 +121,7 @@
MailOutlined,
InteractionOutlined,
AlertOutlined,
GatewayOutlined,
},
props:{
star: {
@ -157,7 +168,7 @@
function clickStar(item){
console.log(item)
updateStarMessage(item);
// update-begin--author:liaozhiyang---date:20240717---for【TV360X-349】通知-标星消息tab列表取消标星后该条信息从标星列表移除
// 代码逻辑说明: 【TV360X-349】通知-标星消息tab列表取消标星后该条信息从标星列表移除
if (item.starFlag == '1' && props.cancelStarAfterDel) {
const findIndex = messageList.value.findIndex((item) => item.id === item.id);
if (findIndex !== -1) {
@ -165,7 +176,6 @@
return;
}
}
// update-end--author:liaozhiyang---date:20240717---for【TV360X-349】通知-标星消息tab列表取消标星后该条信息从标星列表移除
if(item.starFlag=='1'){
item.starFlag = '0'
}else{
@ -173,22 +183,37 @@
}
}
// update-begin-author:taoyan date:2023-5-10 for: QQYUN-4744【系统通知】6、系统通知@人后,对方看不到是哪个表单@的,没有超链接
// 代码逻辑说明: QQYUN-4744【系统通知】6、系统通知@人后,对方看不到是哪个表单@的,没有超链接
const { goPage, currentModal, modalRegCache, bindParams, isFormComment } = useMessageHref(emit, props);
//const emojiIndex = inject('$globalEmojiIndex')
const emojiIndex = getGloablEmojiIndex()
const { getHtml } = useEmojiHtml(emojiIndex);
// update-end-author:taoyan date:2023-5-10 for: QQYUN-4744【系统通知】6、系统通知@人后,对方看不到是哪个表单@的,没有超链接
function showMessageDetail(record){
record.readFlag = '1'
goPage(record);
emit('close', record.id)
//update-begin---author:wangshuai---date:2024-06-11---for:【TV360X-791】收到邮件通知点击回复应该把通知公告列表关闭---
// 代码逻辑说明: 【TV360X-791】收到邮件通知点击回复应该把通知公告列表关闭---
if(record.busType==='email'){
emit('close-modal')
// 代码逻辑说明: 【JHHB-224】【我的消息】点击去处理后应该关闭弹窗---
} else if(['bpm', 'bpm_task', 'tenant_invite'].includes(record.busType)){
//判断是否是查看详情
if (record.msgAbstract) {
try {
const json = JSON.parse(record.msgAbstract);
//查看详情,非去处理
if (json.taskDetail) {
return;
}
} catch (e) {
console.error('getHrefText:msgAbstract参数不是JSON格式', record.msgAbstract);
}
}
emit('close-modal');
} else if (['eoa_co_notify', 'eoa_co_remind', 'eoa_sup_notify', 'eoa_sup_remind'].includes(record.busType)) {
emit('close-modal');
}
//update-end---author:wangshuai---date:2024-06-11---for:【TV360X-791】收到邮件通知点击回复应该把通知公告列表关闭---
}
//返回list列表为空数据时展示的内容
@ -206,11 +231,10 @@
*
*/
function setLocaleText() {
//update-begin---author:wangshuai---date:2024-04-24---for:【QQYUN-9105】未读有问题---
// 代码逻辑说明: 【QQYUN-9105】未读有问题---
let rangeDateKey = searchParams.rangeDateKey;
let value = messageCount.value;
if (value > 0 && !props.star && rangeDateKey && rangeDateKey === '7day') {
//update-end---author:wangshuai---date:2024-04-24---for:【QQYUN-9105】未读有问题---
locale.value = {
emptyText: h(
'span',
@ -272,4 +296,13 @@
cursor: pointer !important;
}
}
.antd-rate{
:deep(.ant-rate-star-first),
:deep(.ant-rate-star-second) {
color: #dddddd;
}
:deep(.ant-rate-star-full .ant-rate-star-second){
color: #fadb14 !important;
}
}
</style>

View File

@ -134,7 +134,7 @@
<user-select-modal isRadioSelection :showButton="false" labelKey="realname" rowKey="username" @register="regModal" @getSelectResult="getSelectedUser"></user-select-modal>
<DetailModal @register="registerDetail" :zIndex="1001" @close="handleDetailColse"/>
<DetailModal @register="registerDetail" :zIndex="1001"/>
</template>
<script>
@ -150,6 +150,8 @@
import folder from '/@/assets/icons/folderNotice.png';
import system from '/@/assets/icons/systemNotice.png';
import flow from '/@/assets/icons/flowNotice.png';
import collaboration from '/@/assets/icons/collaborationNotice.png';
import superviseNotice from '/@/assets/icons/superviseNotice.png';
export default {
name: 'SysMessageModal',
components: {
@ -159,10 +161,9 @@
BellFilled,
ExclamationOutlined,
JSelectUser,
// update-begin--author:liaozhiyang---date:20240308---for【QQYUN-8241】emoji-mart-vue-fast库异步加载
// 代码逻辑说明: 【QQYUN-8241】emoji-mart-vue-fast库异步加载
SysMessageList: createAsyncComponent(() => import('./SysMessageList.vue')),
// SysMessageList,
// update-end--author:liaozhiyang---date:20240308---for【QQYUN-8241】emoji-mart-vue-fast库异步加载
UserSelectModal,
PlusOutlined,
DetailModal
@ -226,7 +227,7 @@
showSearch.value = false
if(data.noticeType){
noticeType.value = data.noticeType;
//update-begin---author:wangshuai---date:2025-07-01---for:【QQYUN-12998】点击完聊天的系统图标再点击系统上面的铃铛就不出数据了---
// 代码逻辑说明: 【QQYUN-12998】点击完聊天的系统图标再点击系统上面的铃铛就不出数据了---
for (const item of noticeTypeOption) {
if(item.key === data.noticeType){
item.active = true;
@ -235,7 +236,6 @@
item.active = false;
}
}
//update-end---author:wangshuai---date:2025-07-01---for:【QQYUN-12998】点击完聊天的系统图标再点击系统上面的铃铛就不出数据了---
delete data.noticeType;
}
//每次弹窗打开 加载最新的数据

View File

@ -104,13 +104,12 @@ export function useSysMessage(setLocaleText) {
starFlag,
id: item.sendId
}
//update-begin-author:taoyan date:2023-3-6 for: QQYUN-4491应用一些小问题 4标星不需要提示吧
// 代码逻辑说明: QQYUN-4491应用一些小问题 4标星不需要提示吧
const data:any = await defHttp.put({url, params}, {isTransformResponse: false});
if(data.success === true){
}else{
createMessage.warning(data.message)
}
//update-end-author:taoyan date:2023-3-6 for: QQYUN-4491应用一些小问题 4标星不需要提示吧
}
@ -138,6 +137,14 @@ export function useSysMessage(setLocaleText) {
return '流程抄送:';
}else if(item.busType=='bpm_task'){
return '流程任务:';
} else if (item.busType == 'eoa_co_remind') {
return '协同催办:';
} else if (item.busType == 'eoa_co_notify') {
return '协同提醒:';
} else if (item.busType == 'eoa_sup_remind') {
return '督办催办:';
} else if (item.busType == 'eoa_sup_notify') {
return '督办提醒:';
} else if (item.msgCategory == '2') {
return '系统消息:';
} else if (item.msgCategory == '1') {
@ -161,6 +168,9 @@ export function useSysMessage(setLocaleText) {
}
}
return '去处理'
} else if (['eoa_co_notify', 'eoa_co_remind', 'eoa_sup_notify', 'eoa_sup_remind'].includes(item.busType)) {
// 代码逻辑说明: JHHB-133消息列表打开协同工作
return '去处理';
} else {
return '查看详情'
}
@ -189,7 +199,15 @@ export function useMessageHref(emit, props){
//const [registerTaskModal, { openModal: openTaskModal }] = useModal();
// 注册表单弹窗
//const [registerDesignFormModal, { openModal: openDesignFormModal }] = useModal();
const messageHrefArray: any[] = getDictItemsByCode('messageHref');
let messageHrefArray: any[] = getDictItemsByCode('messageHref');
// 代码逻辑说明: JHHB-133消息列表打开协同工作
messageHrefArray = [
...messageHrefArray,
{ value: 'eoa_co_remind', text: '/collaboration/pending', url: '/collaboration/launch' },
{ value: 'eoa_co_notify', text: '/collaboration/pending', url: '/collaboration/launch' },
{ value: 'eoa_sup_notify', text: '/superviser/pending' },
{ value: 'eoa_sup_remind', text: '/superviser/pending' },
];
const router = useRouter();
const appStore = useAppStore();
const rt = useRoute();
@ -303,7 +321,7 @@ export function useMessageHref(emit, props){
// 从消息页面列表点击详情查看 直接打开modal
openModalFun()
}
// update-begin-author:taoyan date:2023-5-10 for: QQYUN-4744【系统通知】6、系统通知@人后,对方看不到是哪个表单@的,没有超链接
// 代码逻辑说明: QQYUN-4744【系统通知】6、系统通知@人后,对方看不到是哪个表单@的,没有超链接
}else if(record.busType == 'comment'){
// de
let msgAbstract = record.msgAbstract;
@ -322,7 +340,6 @@ export function useMessageHref(emit, props){
}
}
}
// update-end-author:taoyan date:2023-5-10 for: QQYUN-4744【系统通知】6、系统通知@人后,对方看不到是哪个表单@的,没有超链接
}else if(record.busType == 'tenant_invite'){
if(props.isLowApp===true){
router.push({ name:"myapps-settings-user", query:{ page:'tenantSetting' }})
@ -443,6 +460,13 @@ export function useMessageHref(emit, props){
return;
}
let path = temp[0].text;
if (['eoa_co_notify', 'eoa_co_remind'].includes(busType)) {
if (busId.startsWith('coId-')) {
path = temp[0].url;
} else if (busId.startsWith('nodeId-')) {
path = temp[0].text;
}
}
path = path.replace('{DETAIL_ID}', busId)
//固定参数 detailId 用于查询表单数据
let query:any = {
@ -461,7 +485,7 @@ export function useMessageHref(emit, props){
}
if(query.taskDetail){
// 查看任务详情的弹窗
await showHistory(query.procInsId)
await showHistory(query.procInsId, {taskOriginalId:query.taskId,busType,id:busId,readFlag:record.readFlag})
}else{
// 跳转路由
appStore.setMessageHrefParams(query);
@ -485,13 +509,15 @@ export function useMessageHref(emit, props){
}
//===============================================================================================================
//update-begin-author:taoyan date:2022-12-31 for: QQYUN-3485 【查看流程】做一个查看页面,非办理页面,只通过流程实例参数即可
async function showHistory(processInstanceId) {
// 代码逻辑说明: QQYUN-3485 【查看流程】做一个查看页面,非办理页面,只通过流程实例参数即可
async function showHistory(processInstanceId, data?) {
let { formData, formUrl } = await getTaskInfoForHistory({ processInstanceId });
formData['PROCESS_TAB_TYPE'] = 'history';
handleOpenType('history', {
formData,
formUrl,
isCc: data && data.busType == 'bpm_cc',
record: data,
title: '流程历史',
});
}
@ -553,7 +579,6 @@ export function useMessageHref(emit, props){
function isURL(s) {
return /^http[s]?:\/\/.*/.test(s);
}
//update-end-author:taoyan date:2022-12-31 for: QQYUN-3485 【查看流程】做一个查看页面,非办理页面,只通过流程实例参数即可
//===============================================================================================================
return {

View File

@ -16,9 +16,8 @@
const title = ref<string>('');
const isUpdate = ref<boolean>(false);
// 注册 form
//update-begin---author:wangshuai ---date:20221123 for[VUEN-2807]消息模板加一个查看功能------------
// 代码逻辑说明: [VUEN-2807]消息模板加一个查看功能------------
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema, setProps }] = useForm({
//update-end---author:wangshuai ---date:20221123 for[VUEN-2807]消息模板加一个查看功能--------------z
schemas: formSchemas,
showActionButtonGroup: false,
baseRowStyle: {

View File

@ -95,12 +95,11 @@
function onDelete(record) {
if (record) {
//update-begin-author:taoyan date:2022-7-14 for: VUEN-1652【bug】应用状态下不允许删除
// 代码逻辑说明: VUEN-1652【bug】应用状态下不允许删除
if(record.useStatus == '1'){
createMessage.warning('该模板已被应用禁止删除!');
return;
}
//update-end-author:taoyan date:2022-7-14 for: VUEN-1652【bug】应用状态下不允许删除
doDeleteDepart([record.id], false);
}
}
@ -125,14 +124,13 @@
async function onDeleteBatch() {
try {
//update-begin-author:taoyan date:2022-7-14 for: VUEN-1652【bug】应用状态下不允许删除
// 代码逻辑说明: VUEN-1652【bug】应用状态下不允许删除
let arr = toRaw(selectedRows.value);
let temp = arr.filter(item=>item.useStatus=='1')
if(temp.length>0){
createMessage.warning('选中的模板已被应用禁止删除!');
return;
}
//update-end-author:taoyan date:2022-7-14 for: VUEN-1652【bug】应用状态下不允许删除
await doDeleteDepart(selectedRowKeys);
selectedRowKeys.value = [];
} finally {
@ -148,9 +146,8 @@
* 操作栏
*/
function getTableAction(record): ActionItem[] {
//update-begin---author:wangshuai ---date:20221123 for[VUEN-2807]消息模板加一个查看功能------------
// 代码逻辑说明: [VUEN-2807]消息模板加一个查看功能------------
return [{ label: '查看', onClick: handleDetail.bind(null, record)}, { label: '编辑', onClick: onEdit.bind(null, record) }];
//update-end---author:wangshuai ---date:20221123 for[VUEN-2807]消息模板加一个查看功能------------
}
/**

View File

@ -76,6 +76,7 @@ export const formSchemas: FormSchema[] = [
label: '模板编码',
field: 'templateCode',
component: 'Input',
required: true,
dynamicRules: ({ model, schema }) => {
return [ ...rules.duplicateCheckRule('sys_sms_template', 'template_code', model, schema, true)];
},

View File

@ -53,12 +53,10 @@
});
record.value = data.record;
} else {
// update-begin--author:liaozhiyang---date:20250807---for【JHHB-128】转公告
//表单赋值
await setFieldsValue({
...data.record,
});
// update-end--author:liaozhiyang---date:20250807---for【JHHB-128】转公告
}
});
//设置标题
@ -69,13 +67,12 @@
let values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
//update-begin-author:liusq---date:20230404--for: [issue#429]新增通知公告提交指定用户参数有undefined ---
// 代码逻辑说明: [issue#429]新增通知公告提交指定用户参数有undefined ---
if(values.msgType==='ALL'){
values.userIds = '';
}else{
values.userIds += ',';
}
//update-end-author:liusq---date:20230404--for: [issue#429]新增通知公告提交指定用户参数有undefined ---
if (isUpdate.value && record.value.sendStatus != '2') {
values.sendStatus = '0';
}

View File

@ -57,7 +57,8 @@
columns: columns,
formConfig: {
schemas: searchFormSchema,
},
fieldMapToTime: [['sendTime', ['sendTime_begin', 'sendTime_end'], 'YYYY-MM-DD']]
}
},
exportConfig: {
name: '消息通知列表',
@ -198,13 +199,12 @@
}
onMounted(() => {
// update-begin--author:liaozhiyang---date:20250807---for【JHHB-128】转公告
// 代码逻辑说明: 【JHHB-128】转公告
const params = appStore.getMessageHrefParams;
if (params?.add) {
delete params.add;
handleAdd(params);
appStore.setMessageHrefParams('');
}
// update-begin--author:liaozhiyang---date:20250807---for【JHHB-128】转公告
});
</script>

View File

@ -65,7 +65,37 @@ export const searchFormSchema: FormSchema[] = [
field: 'titile',
label: '标题',
component: 'JInput',
colProps: { span: 8 },
colProps: { span: 6 },
},
{
field: 'msgCategory',
label: '消息类型',
component: 'JDictSelectTag',
defaultValue: '1',
componentProps: {
dictCode: 'msg_category',
placeholder: '请选择类型',
},
colProps: { span: 6 },
},
{
field: 'msgClassify',
label: '公告分类',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'notice_type',
placeholder: '请选择公告分类',
},
colProps: { span: 6 },
},
{
field: 'sendTime',
label: '发布时间',
component: 'RangePicker',
componentProps: {
valueType: 'Date',
},
colProps: { span: 6 },
},
];
@ -111,7 +141,7 @@ export const formSchema: FormSchema[] = [
componentProps: {
placeholder: '请输入标题',
},
// update-begin--author:liaozhiyang---date:20240701---for【TV360X-1632】标题过长保存报错长度校验
// 代码逻辑说明: 【TV360X-1632】标题过长保存报错长度校验
dynamicRules() {
return [
{
@ -126,7 +156,6 @@ export const formSchema: FormSchema[] = [
},
];
},
// update-end--author:liaozhiyang---date:20240701---for【TV360X-1632】标题过长保存报错长度校验
},
{
field: 'msgAbstract',
@ -171,9 +200,8 @@ export const formSchema: FormSchema[] = [
required: true,
componentProps: {
rowKey: 'id',
// update-begin--author:liaozhiyang---date:20240701---for【TV360X-1627】通知公告用户选择组件没翻译
// 代码逻辑说明: 【TV360X-1627】通知公告用户选择组件没翻译
labelKey: 'realname',
// update-end--author:liaozhiyang---date:20240701---for【TV360X-1627】通知公告用户选择组件没翻译
},
ifShow: ({ values }) => values.msgType == 'USER',
},
@ -291,7 +319,7 @@ export function getBpmFormSchema(_formData): FormSchema[] {
componentProps: {
placeholder: '请输入标题',
},
// update-begin--author:liaozhiyang---date:20240701---for【TV360X-1632】标题过长保存报错长度校验
// 代码逻辑说明: 【TV360X-1632】标题过长保存报错长度校验
dynamicRules() {
return [
{
@ -306,7 +334,6 @@ export function getBpmFormSchema(_formData): FormSchema[] {
},
];
},
// update-end--author:liaozhiyang---date:20240701---for【TV360X-1632】标题过长保存报错长度校验
},
{
field: 'msgAbstract',
@ -333,9 +360,8 @@ export function getBpmFormSchema(_formData): FormSchema[] {
required: true,
componentProps: {
rowKey: 'id',
// update-begin--author:liaozhiyang---date:20240701---for【TV360X-1627】通知公告用户选择组件没翻译
// 代码逻辑说明: 【TV360X-1627】通知公告用户选择组件没翻译
labelKey: 'realname',
// update-end--author:liaozhiyang---date:20240701---for【TV360X-1627】通知公告用户选择组件没翻译
},
ifShow: ({ values }) => values.msgType == 'USER',
},

View File

@ -80,7 +80,6 @@
function handleView(record) {
if (record && record.url) {
console.log('glob.onlineUrl', glob.viewUrl);
//update-begin---author:scott ---date:2024-06-03 for【TV360X-952】升级到kkfileview4.1.0---
// let filePath = encodeURIComponent(record.url);
let url = encodeURIComponent(encryptByBase64(record.url));
// //文档采用pdf预览高级模式
@ -88,7 +87,6 @@
// filePath = filePath
// }
let previewUrl = `${glob.viewUrl}?url=` + url;
//update-end---author:scott ---date:2024-06-03 for【TV360X-952】升级到kkfileview4.1.0---
window.open(previewUrl, '_blank');
}

View File

@ -120,10 +120,9 @@
*/
async function batchHandleDelete() {
await batchDeletePosition({ ids: selectedRowKeys.value }, () => {
// update-begin--author:liaozhiyang---date:20240223---for【QQYUN-8334】批量删除之后按钮未隐藏选中记录还在
// 代码逻辑说明: 【QQYUN-8334】批量删除之后按钮未隐藏选中记录还在
selectedRowKeys.value = [];
reload();
// update-end--author:liaozhiyang---date:20240223---for【QQYUN-8334】批量删除之后按钮未隐藏选中记录还在
});
}
</script>

View File

@ -84,11 +84,9 @@
roleId.value = data.roleId;
//初始化数据
const roleResult = await queryTreeListForRole();
// update-begin--author:liaozhiyang---date:20240228---for【QQYUN-8355】角色权限配置的菜单翻译
// 代码逻辑说明: 【QQYUN-8355】角色权限配置的菜单翻译
treeData.value = translateTitle(roleResult.treeList);
// update-end--author:liaozhiyang---date:20240228---for【QQYUN-8355】角色权限配置的菜单翻译
allTreeKeys.value = roleResult.ids;
// update-begin--author:liaozhiyang---date:20240531---for【TV360X-590】角色授权弹窗操作缓存
const localData = localStorage.getItem(ROLE_AUTH_CONFIG_KEY);
if (localData) {
const obj = JSON.parse(localData);
@ -97,7 +95,6 @@
} else {
expandedKeys.value = roleResult.ids;
}
// update-end--author:liaozhiyang---date:20240531---for【TV360X-590】角色授权弹窗操作缓存
//初始化角色菜单数据
const permResult = await queryRolePermission({ roleId: unref(roleId) });
checkedKeys.value = permResult;
@ -218,7 +215,7 @@
permissionIds: unref(getTree().getCheckedKeys()).join(','),
lastpermissionIds: unref(defaultCheckedKeys).join(','),
};
//update-begin-author:taoyan date:2023-2-11 for: issues/352 VUE角色授权重复保存
// 代码逻辑说明: issues/352 VUE角色授权重复保存
if(loading.value===false){
await doSave(params)
}else{
@ -246,7 +243,6 @@
loading.value = false;
}, 500)
}
//update-end-author:taoyan date:2023-2-11 for: issues/352 VUE角色授权重复保存
/**
* 树菜单选择
@ -290,7 +286,7 @@
position: absolute;
width: 618px;
}
//update-begin---author:wangshuai ---date:20230202 for抽屉弹窗标题图标下拉样式------------
// 代码逻辑说明: 抽屉弹窗标题图标下拉样式------------
.line {
height: 1px;
width: 100%;
@ -307,5 +303,4 @@
:deep(.jeecg-tree-header) {
border-bottom: none;
}
//update-end---author:wangshuai ---date:20230202 for抽屉弹窗标题图标下拉样式------------
</style>

View File

@ -62,9 +62,8 @@
api: userList,
columns: userColumns,
formConfig: {
//update-begin---author:wangshuai ---date:20230703 for【QQYUN-5685】3、租户角色下,查询居左显示
// 代码逻辑说明: 【QQYUN-5685】3、租户角色下,查询居左显示
labelWidth: 60,
//update-end---author:wangshuai ---date:20230703 for【QQYUN-5685】3、租户角色下,查询居左显示
schemas: searchUserFormSchema,
autoSubmitOnEnter: true,
},
@ -145,10 +144,9 @@
*/
async function batchHandleDelete() {
await batchDeleteUserRole({ userIds: checkedKeys.value.join(','), roleId: roleId.value }, () => {
// update-begin--author:liaozhiyang---date:20240701---for【TV360X-1655】批量取消关联之后清空选中记录
// 代码逻辑说明: 【TV360X-1655】批量取消关联之后清空选中记录
reload();
checkedKeys.value = [];
// update-end--author:liaozhiyang---date:20240701---for【TV360X-1655】批量取消关联之后清空选中记录
});
}

View File

@ -62,10 +62,9 @@
api: list,
columns: columns,
formConfig: {
// update-begin--author:liaozhiyang---date:20230803---for【QQYUN-5873】查询区域lablel默认居左
// 代码逻辑说明: 【QQYUN-5873】查询区域lablel默认居左
labelWidth:65,
rowProps: { gutter: 24 },
// update-end--author:liaozhiyang---date:20230803---for【QQYUN-5873】查询区域lablel默认居左
schemas: searchFormSchema,
},
actionColumn: {

View File

@ -82,7 +82,7 @@ export const saveOrUpdateRole = (params, isUpdate) => {
* 编码校验
* @param params
*/
// update-begin--author:liaozhiyang---date:20231215---for【QQYUN-7415】表单调用接口进行校验的添加防抖
// 代码逻辑说明: 【QQYUN-7415】表单调用接口进行校验的添加防抖
let timer;
export const isRoleExist = (params) => {
return new Promise((resolve, rejected) => {
@ -99,7 +99,6 @@ export const isRoleExist = (params) => {
}, 500);
});
};
// update-end--author:liaozhiyang---date:20231215---for【QQYUN-7415】表单调用接口进行校验的添加防抖
/**
* 根据角色查询树信息
*/

View File

@ -29,9 +29,8 @@ const { prefixCls, tableContext } = useListPage({
immediate:false,
formConfig: {
schemas: userSearchFormSchema,
//update-begin---author:wangshuai ---date:20230704 for【QQYUN-5698】样式问题------------
// 代码逻辑说明: 【QQYUN-5698】样式问题------------
labelWidth: 40,
//update-end---author:wangshuai ---date:20230704 for【QQYUN-5698】样式问题------------
actionColOptions: {
xs: 24,
sm: 8,

View File

@ -160,14 +160,13 @@
* @param userSelectId
*/
async function handleInviteUserOk(phone, username) {
//update-begin---author:wangshuai ---date:20230314 for【QQYUN-4605】后台的邀请谁加入租户没办法选不是租户下的用户------------
// 代码逻辑说明: 【QQYUN-4605】后台的邀请谁加入租户没办法选不是租户下的用户------------
if (phone) {
await invitationUserJoin({ ids: selectedRowKeys.value.join(','), phone: phone });
}
if (username) {
await invitationUserJoin({ ids: selectedRowKeys.value.join(','), username: username });
}
//update-end---author:wangshuai ---date:20230314 for【QQYUN-4605】后台的邀请谁加入租户没办法选不是租户下的用户------------
}
/**

View File

@ -11,12 +11,13 @@
<div class="common-info-row">
<div class="common-info-row-label">组织LOGO</div>
<div class="common-info-row-content">
<img :src="getImageSrc()" style="width: 100px;cursor: pointer" @click="previewImage">
<JImageUpload v-model:value="formState.companyLogo" @change="handleCompanyLogoChange"></JImageUpload>
</div>
</div>
<div class="common-info-row m-top24">
<div class="common-info-row-label">组织名称</div>
<span class="m-right16">{{ formState.name }}</span>
<span class="edit-name" @click="goUpdate('name')">修改</span>
</div>
<div class="common-info-row m-top24">
<div class="common-info-row-label">组织门牌号</div>
@ -38,14 +39,17 @@
<div class="common-info-row">
<div class="common-info-row-label">所在地</div>
<span class="m-right16">{{ formState.companyAddress_dictText }}</span>
<span class="edit-name" @click="goUpdate('companyAddress')">修改</span>
</div>
<div class="common-info-row m-top24">
<div class="common-info-row-label">所在行业</div>
<span class="m-right16">{{ formState.trade_dictText }}</span>
<span class="edit-name" @click="goUpdate('trade')">修改</span>
</div>
<div class="common-info-row m-top24">
<div class="common-info-row-label">工作地点</div>
<span class="m-right16">{{ formState.workPlace }}</span>
<span class="edit-name" @click="goUpdate('workPlace')">修改</span>
</div>
<div class="cancel-split-line"></div>
</div>
@ -53,16 +57,68 @@
</a-form>
</div>
</div>
<!-- 组织名称修改弹窗 -->
<a-modal v-model:open="modalVisible.name" title="修改组织名称" width="500" destroy-on-close @ok="doUpdate('name')">
<a-form ref="manageNameRef" :model="updateInfo" :rules="getManageNameRules">
<a-form-item name="name" class="form-item-padding">
<div class="form-group">
<span class="form-label">
组织名称
<span class="txt-middle red">*</span>
</span>
<a-input v-model:value="updateInfo.name" />
</div>
</a-form-item>
</a-form>
</a-modal>
<!-- 组织所在地弹窗 -->
<a-modal v-model:open="modalVisible.companyAddress" title="所在地" width="500" destroy-on-close @ok="doUpdate('companyAddress')">
<a-form :model="updateInfo">
<a-form-item name="companyAddress" class="form-item-padding">
<div style="margin-top: 20px">
<j-area-select v-model:value="updateInfo.companyAddress" />
</div>
</a-form-item>
</a-form>
</a-modal>
<!-- 组织所在行业弹窗 -->
<a-modal v-model:open="modalVisible.trade" title="设置所在行业" width="500" destroy-on-close @ok="doUpdate('trade')">
<a-form :model="updateInfo">
<a-form-item name="trade" class="form-item-padding">
<div style="margin-top: 20px">
<j-dict-select-tag v-model:value="updateInfo.trade" dictCode="trade" />
</div>
</a-form-item>
</a-form>
</a-modal>
<!-- 工作地点弹窗 -->
<a-modal v-model:open="modalVisible.workPlace" title="设置工作地点" width="500" destroy-on-close @ok="doUpdate('workPlace')">
<a-form ref="workPlaceRef" :model="updateInfo">
<a-form-item name="name" class="form-item-padding">
<div style="margin-top: 20px">
<a-textarea placeholder="请填写工作地点" v-model:value="updateInfo.workPlace" />
</div>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" name="tenant-my-tenant-list" setup>
import { onMounted, reactive } from 'vue';
import { onMounted, reactive, ref } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage';
import {getFileAccessHttpUrl, tenantSaasMessage} from '@/utils/common/compUtils';
import { getTenantById } from '@/views/system/tenant/tenant.api';
import { getTenantById, saveOrUpdateTenant } from '@/views/system/tenant/tenant.api';
import { getTenantId } from '@/utils/auth';
import { getDataByCode, getRealCode, provinceOptions } from '@/components/Form/src/utils/areaDataUtil';
import { initDictOptions } from '@/utils/dict';
import {createImgPreview} from "@/components/Preview";
import { JImageUpload } from "@/components/Form";
// import {updateTenantInfo} from "@/views/super/myapps/organization/organization.api";
import { defHttp } from "/@/utils/http/axios";
import JAreaSelect from "/@/components/Form/src/jeecg/components/JAreaSelect.vue";
import JDictSelectTag from "/@/components/Form/src/jeecg/components/JDictSelectTag.vue";
const { createMessage } = useMessage();
const formState = reactive({
@ -76,7 +132,28 @@
companyLogo: '',
});
let tradeOptions: any[] = [];
//组织名称ref
const manageNameRef= ref();
// modal显示
const modalVisible = reactive<any>({
name: false,
trade: false,
companyAddress: false
});
// 组织名称检验规则
const getManageNameRules = {
name: [{ required: true, message: '组织名称不能为空', trigger: 'blur' }],
};
//修改对象
const updateInfo = reactive<any>({
name: '',
trade:'',
companyAddress: '',
workPlace: '',
});
/**
* 初始化租户信息
*/
@ -140,18 +217,55 @@
}
/**
* 获取图片路径
* 公司logo上传成功事件
*
* @param val
*/
function getImageSrc() {
return getFileAccessHttpUrl(formState.companyLogo) || "";
function handleCompanyLogoChange(val) {
if(val){
saveOrUpdateTenant({ id: formState.id, companyLogo: val }, true)
}
}
/**
* 预览图片
* 更新打开弹窗
*
* @param key
*/
function previewImage() {
let fileAccessHttpUrl = getFileAccessHttpUrl(formState.companyLogo);
createImgPreview({ imageList: [fileAccessHttpUrl], defaultWidth: 700, rememberState: true });
function goUpdate(key){
modalVisible[key] = true;
updateInfo[key] = formState[key];
}
/**
* 编辑租户信息
* @param params
*/
async function updateTenantInfo(params){
return defHttp.put({ url: '/sys/tenant/editOwnTenant', params });
}
/**
* 更新数据
* @param key
*/
async function doUpdate(key) {
if(key=='name'){
await manageNameRef.value.validateFields();
}
//所在地为空报错
if(key == 'companyAddress'){
if(updateInfo[key] instanceof Array){
updateInfo[key] = '';
}
}
let params = {
id: formState.id,
[key]: updateInfo[key]
};
await updateTenantInfo(params);
initTenant();
modalVisible[key] = false
}
onMounted(() => {
@ -293,4 +407,26 @@
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.ant-upload.ant-upload-select){
width: 80px !important;
height: 80px !important;
border: unset !important;
}
:deep(.ant-upload-list-item-container){
width: 80px !important;
height: 80px !important;
border: unset !important;
}
.edit-name {
border: none;
border-radius: 3px;
box-sizing: border-box;
color: #1e88e5;
cursor: pointer;
display: inline-block;
outline: none;
text-shadow: none;
user-select: none;
vertical-align: middle;
}
</style>

View File

@ -0,0 +1,272 @@
<template>
<BasicDrawer v-bind="$attrs" @register="registerDrawer" width="650px" destroyOnClose showFooter>
<template #title>
权限配置
<a-dropdown>
<a-button class="more-icon">
更多操作
<Icon icon="ant-design:down-outlined" size="14px" style="position: relative; top: 1px; right: 5px"></Icon>
</a-button>
<template #overlay>
<a-menu @click="treeMenuClick">
<a-menu-item key="checkAll">选择全部</a-menu-item>
<a-menu-item key="cancelCheck">取消选择</a-menu-item>
<div class="line"></div>
<a-menu-item key="openAll">展开全部</a-menu-item>
<a-menu-item key="closeAll">折叠全部</a-menu-item>
<div class="line"></div>
<a-menu-item key="relation">层级关联</a-menu-item>
<a-menu-item key="standAlone">层级独立</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<BasicTree
ref="treeRef"
checkable
:treeData="treeData"
:checkedKeys="checkedKeys"
:expandedKeys="expandedKeys"
:selectedKeys="selectedKeys"
:clickRowToExpand="false"
:checkStrictly="true"
title="所拥有的的权限"
@check="onCheck"
>
<template #title="{ slotTitle, ruleFlag }">
{{ slotTitle }}
<Icon v-if="ruleFlag" icon="ant-design:align-left-outlined" style="margin-left: 5px; color: red"></Icon>
</template>
</BasicTree>
<!--右下角按钮-->
<template #footer>
<!-- <PopConfirmButton title="确定放弃编辑?" @confirm="closeDrawer" okText="确定" cancelText="取消"></PopConfirmButton> -->
<a-button @click="closeDrawer">取消</a-button>
<a-button @click="handleSubmit(false)" type="primary" :loading="loading" ghost style="margin-right: 0.8rem">仅保存</a-button>
<a-button @click="handleSubmit(true)" type="primary" :loading="loading">保存并关闭</a-button>
</template>
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { BasicTree, TreeItem } from '/@/components/Tree';
import { queryPremTreeList, editPackPermission } from '../tenant.api';
import { useI18n } from '/@/hooks/web/useI18n';
import { PACK_AUTH_CONFIG_KEY } from '/@/enums/cacheEnum';
const emit = defineEmits(['register','success']);
//树的信息
const treeData = ref<TreeItem[]>([]);
//树的全部节点信息
const allTreeKeys = ref([]);
//树的选择节点信息
const checkedKeys = ref<any>([]);
//树的选中的节点信息
const selectedKeys = ref([]);
const packId = ref('');
//树的实例
const treeRef = ref(null);
const loading = ref(false);
//展开折叠的key
const expandedKeys = ref<any>([]);
//父子节点选中状态是否关联 true不关联false关联
const checkStrictly = ref<boolean>(false);
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
await reset();
setDrawerProps({ confirmLoading: false, loading: true });
packId.value = data.packId;
//初始化数据
const roleResult = await queryPremTreeList();
treeData.value = translateTitle(roleResult.treeList);
allTreeKeys.value = roleResult.ids;
const localData = localStorage.getItem(PACK_AUTH_CONFIG_KEY);
if (localData) {
const obj = JSON.parse(localData);
obj.level && treeMenuClick({ key: obj.level });
obj.expand && treeMenuClick({ key: obj.expand });
} else {
expandedKeys.value = roleResult.ids;
}
//初始化角色菜单数据
if(data.permissionIds){
checkedKeys.value = data.permissionIds.split(",");
}
setDrawerProps({ loading: false });
});
/**
* 翻译菜单名称
*/
function translateTitle(data) {
if (data?.length) {
data.forEach((item) => {
if (item.slotTitle) {
const { t } = useI18n();
if (item.slotTitle.includes("t('") && t) {
item.slotTitle = new Function('t', `return ${item.slotTitle}`)(t);
}
}
if (item.children?.length) {
translateTitle(item.children);
}
});
}
return data;
}
/**
* 点击选中
*/
function onCheck(o, e) {
// checkStrictly: true=>层级独立false=>层级关联.
if (checkStrictly.value) {
checkedKeys.value = o.checked ? o.checked : o;
} else {
const keys = getNodeAllKey(e.node, 'children', 'key');
if (e.checked) {
// 反复操作下可能会有重复的keys得用new Set去重下
checkedKeys.value = [...new Set([...checkedKeys.value, ...keys])];
} else {
const result = removeMatchingItems(checkedKeys.value, keys);
checkedKeys.value = result;
}
}
}
/**
* 删除相匹配数组的项
*/
function removeMatchingItems(arr1, arr2) {
// 使用哈希表记录 arr2 中的元素
const hashTable = {};
for (const item of arr2) {
hashTable[item] = true;
}
// 使用 filter 方法遍历第一个数组,过滤出不在哈希表中存在的项
return arr1.filter((item) => !hashTable[item]);
}
/**
* 获取当前节点及以下所有子孙级的key
*/
function getNodeAllKey(node: any, children: any, key: string) {
const result: any = [];
result.push(node[key]);
const recursion = (data) => {
data.forEach((item: any) => {
result.push(item[key]);
if (item[children]?.length) {
recursion(item[children]);
}
});
};
node[children]?.length && recursion(node[children]);
return result;
}
/**
* 数据重置
*/
function reset() {
treeData.value = [];
allTreeKeys.value = [];
checkedKeys.value = [];
selectedKeys.value = [];
packId.value = '';
}
/**
* 获取tree实例
*/
function getTree() {
const tree = unref(treeRef);
if (!tree) {
throw new Error('tree is null!');
}
return tree;
}
/**
* 提交
*/
async function handleSubmit(exit) {
let params = {
id: unref(packId),
permissionIds: unref(getTree()?.getCheckedKeys()).join(','),
};
if (loading.value === false) {
await doSave(params);
} else {
console.log('请等待上次执行完毕!');
}
if (exit) {
// 如果关闭
closeDrawer();
}
}
// VUE角色授权重复保存 #352
async function doSave(params) {
loading.value = true;
try {
await editPackPermission(params);
emit("success")
} catch (e) {
loading.value = false;
}
setTimeout(() => {
loading.value = false;
}, 500);
}
/**
* 树菜单选择
* @param key
*/
function treeMenuClick({ key }) {
if (key === 'checkAll') {
checkedKeys.value = allTreeKeys.value;
} else if (key === 'cancelCheck') {
checkedKeys.value = [];
} else if (key === 'openAll') {
expandedKeys.value = allTreeKeys.value;
saveLocalOperation('expand', 'openAll');
} else if (key === 'closeAll') {
expandedKeys.value = [];
saveLocalOperation('expand', 'closeAll');
} else if (key === 'relation') {
checkStrictly.value = false;
saveLocalOperation('level', 'relation');
} else {
checkStrictly.value = true;
saveLocalOperation('level', 'standAlone');
}
}
/**
* 角色授权弹窗操作缓存
*/
const saveLocalOperation = (key, value) => {
const localData = localStorage.getItem(PACK_AUTH_CONFIG_KEY);
const obj = localData ? JSON.parse(localData) : {};
obj[key] = value;
localStorage.setItem(PACK_AUTH_CONFIG_KEY, JSON.stringify(obj));
};
</script>
<style lang="less" scoped>
/** 固定操作按钮 */
.jeecg-basic-tree {
position: absolute;
width: 618px;
}
.line {
height: 1px;
width: 100%;
border-bottom: 1px solid #f0f0f0;
}
.more-icon {
float: right;
margin-right: 2px;
cursor: pointer;
}
:deep(.jeecg-tree-header) {
border-bottom: none;
}
</style>

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