新增 CLAUDE.md 文件以提供项目指导,添加 .claudeignore 文件以排除不必要的文件,更新 pom.xml 版本至 3.9.2,修复多个路径遍历和 SQL 注入漏洞,优化字典翻译切面逻辑,增强文件上传和下载的安全性,新增音频文件类型支持,改进动态数据源的安全校验。
This commit is contained in:
@@ -1,21 +1,29 @@
|
||||
<template>
|
||||
<PageWrapper title="富文本组件示例">
|
||||
<Tinymce v-model="value" @change="handleChange" width="100%" />
|
||||
</PageWrapper>
|
||||
<div :style="{ height: contentHeight, overflowY: 'scroll' }">
|
||||
<PageWrapper title="富文本组件示例">
|
||||
<Tinymce v-model="value" @change="handleChange" width="100%" />
|
||||
<div style="height: 1000px"></div>
|
||||
</PageWrapper>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { defineComponent, ref, computed } from 'vue';
|
||||
import { Tinymce } from '/@/components/Tinymce/index';
|
||||
import { PageWrapper } from '/@/components/Page';
|
||||
|
||||
import { useLayoutHeight } from '@/layouts/default/content/useContentViewHeight';
|
||||
export default defineComponent({
|
||||
components: { Tinymce, PageWrapper },
|
||||
setup() {
|
||||
const value = ref('hello world!');
|
||||
const { headerHeightRef } = useLayoutHeight();
|
||||
function handleChange(value: string) {
|
||||
console.log(value);
|
||||
}
|
||||
return { handleChange, value };
|
||||
//【issues/9448】
|
||||
const contentHeight = computed(() => {
|
||||
return `calc(100vh - ${headerHeightRef.value}px)`;
|
||||
});
|
||||
return { handleChange, value, contentHeight };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
<template>
|
||||
<PageWrapper title="Icon组件示例">
|
||||
<PageWrapper title="图标使用示例">
|
||||
<CollapseContainer title="Icon组件中iconfiy图标使用(不推荐使用,项目中所有图标都构建在一个js且首屏就加载,没有分割到chuck中,不是按需引入)" class="my-5">
|
||||
<div class="flex justify-around flex-wrap">
|
||||
<Icon icon="ion:layers-outline" :size="30" />
|
||||
<Icon icon="ion:bar-chart-outline" :size="30" />
|
||||
<Icon icon="ion:tv-outline" :size="30" />
|
||||
<Icon icon="ion:settings-outline" :size="30" />
|
||||
<Icon icon="ion:language" :size="30" />
|
||||
</div>
|
||||
</CollapseContainer>
|
||||
|
||||
<CollapseContainer title="推荐直接使用iconify原生组件(分割到chunk中,按需引入)" class="my-5">
|
||||
<div class="flex justify-around flex-wrap">
|
||||
<IconifyonLayersOutline class="text-30px" />
|
||||
<IconifyIonBarChartOutline class="text-30px" />
|
||||
<IconifyIonTvOutline class="text-30px" />
|
||||
<IconifyIonSettingsOutline class="text-30px" />
|
||||
<IconIonLanguage class="text-30px" />
|
||||
</div>
|
||||
</CollapseContainer>
|
||||
|
||||
<CollapseContainer title="Antv Icon使用 (直接按需引入相应组件即可)">
|
||||
<div class="flex justify-around">
|
||||
<GithubFilled :style="{ fontSize: '30px' }" />
|
||||
@@ -12,15 +32,6 @@
|
||||
</div>
|
||||
</CollapseContainer>
|
||||
|
||||
<CollapseContainer title="IconIfy 组件使用" class="my-5">
|
||||
<div class="flex justify-around flex-wrap">
|
||||
<Icon icon="ion:layers-outline" :size="30" />
|
||||
<Icon icon="ion:bar-chart-outline" :size="30" />
|
||||
<Icon icon="ion:tv-outline" :size="30" />
|
||||
<Icon icon="ion:settings-outline" :size="30" />
|
||||
</div>
|
||||
</CollapseContainer>
|
||||
|
||||
<CollapseContainer title="svg 雪碧图" class="my-5">
|
||||
<div class="flex justify-around flex-wrap">
|
||||
<SvgIcon name="test" size="32" />
|
||||
@@ -68,7 +79,7 @@
|
||||
|
||||
import { openWindow } from '/@/utils';
|
||||
import { PageWrapper } from '/@/components/Page';
|
||||
|
||||
import IconIonLanguage from '~icons/ion/language'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
PageWrapper,
|
||||
@@ -84,6 +95,7 @@
|
||||
Alert,
|
||||
IconPicker,
|
||||
SvgIcon,
|
||||
IconIonLanguage,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
|
||||
@@ -46,6 +46,15 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #planTimeRangeSlot="{ row, triggerChange }">
|
||||
<a-range-picker
|
||||
:value="row.planTimeRange"
|
||||
value-format="YYYY-MM-DD"
|
||||
:bordered="false"
|
||||
@change="(dates) => triggerChange(dates)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #myAction="props">
|
||||
<a @click="onLookRow(props)">查看</a>
|
||||
<a-divider type="vertical" />
|
||||
@@ -212,6 +221,36 @@
|
||||
customValue: ['Y', 'N'], // true ,false
|
||||
defaultChecked: false,
|
||||
},
|
||||
{
|
||||
title: '预估开始日期 ~ 预估结束日期',
|
||||
key: 'planTimeRange',
|
||||
type: JVxeTypes.slot,
|
||||
width: 280,
|
||||
slotName: 'planTimeRangeSlot',
|
||||
},
|
||||
{
|
||||
title: '自定义树控件',
|
||||
key: 'sel_tree_demo',
|
||||
type: JVxeTypes.treeSelect,
|
||||
width: 200,
|
||||
// dict 格式:表名,文本字段,存储字段
|
||||
dict: 'sys_category,name,id',
|
||||
pidField: 'pid',
|
||||
pidValue: '0',
|
||||
hasChildField: 'has_child',
|
||||
multiple: true,
|
||||
placeholder: '请选择',
|
||||
},
|
||||
{
|
||||
title: '分类字典树',
|
||||
key: 'cat_tree_demo',
|
||||
type: JVxeTypes.catTreeSelect,
|
||||
width: 200,
|
||||
// pcode: 根分类编码,'0' 表示加载所有根节点
|
||||
pcode: 'B01',
|
||||
multiple: true,
|
||||
placeholder: '请选择',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
@@ -261,6 +300,7 @@
|
||||
select_search: options[random(0, 3)],
|
||||
datetime: randomDatetime(),
|
||||
checkbox: ['Y', 'N'][random(0, 1)],
|
||||
planTimeRange: [dayjs().subtract(random(1, 30), 'day').format('YYYY-MM-DD'), dayjs().add(random(1, 30), 'day').format('YYYY-MM-DD')],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<li>2. 使用 sortKey 属性可以自定义排序保存的 key,默认为 orderNum。</li>
|
||||
<li>3. 使用 sortBegin 属性可以自定义排序的起始值,默认为 0。</li>
|
||||
<li>4. sortKey 定义的字段不需要定义在 columns 中也能正常获取到值。</li>
|
||||
<li>5. 当存在 fixed 列时,拖拽排序将会失效,仅能上下排序。</li>
|
||||
</ol>
|
||||
|
||||
<p> 以下示例开启了拖拽排序,排序值保存字段为 sortNum,排序起始值为 3<br /> </p>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<JPopup
|
||||
v-model:value="model[field]"
|
||||
:formElRef="formElRef"
|
||||
code="ces_app_rep001"
|
||||
code="withparamreport"
|
||||
:param="{ sex: '1' }"
|
||||
:fieldConfig="[{ source: 'name', target: 'pop2' }]"
|
||||
/>
|
||||
@@ -57,6 +57,9 @@
|
||||
<template #superQuery1="{ model, field }">
|
||||
<super-query :config="superQueryConfig" @search="(value)=>handleSuperQuery(value, model, field)" :isCustomSave="true" :saveSearchData="saveSearchData" :save="handleSuperQuerySave"/>
|
||||
</template>
|
||||
<template #tabsSelectUser="{ model, field }">
|
||||
<JTabsSelectUser v-model:value="model[field]" ></JTabsSelectUser>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
@@ -68,7 +71,7 @@
|
||||
import { schemas } from './jeecgComponents.data';
|
||||
import { usePermission } from '/@/hooks/web/usePermission';
|
||||
import { BasicDragVerify } from '/@/components/Verify';
|
||||
|
||||
import JTabsSelectUser from '/@/components/jeecg/JTabsSelectUser/index.vue';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasicForm,
|
||||
@@ -79,6 +82,7 @@
|
||||
JCheckbox,
|
||||
JInput,
|
||||
JEllipsis,
|
||||
JTabsSelectUser,
|
||||
BasicDragVerify,
|
||||
},
|
||||
name: 'JeecgComponents',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FormSchema, JCronValidator } from '/@/components/Form';
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import JCronValidator from '/@/components/Form/src/jeecg/components/JEasyCron/validator';
|
||||
import { usePermission } from '/@/hooks/web/usePermission';
|
||||
|
||||
const { isDisabledAuth } = usePermission();
|
||||
@@ -935,7 +936,20 @@ export const schemas: FormSchema[] = [
|
||||
label: '选中值',
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
|
||||
{
|
||||
field: 'tabsSelectUser',
|
||||
component: 'Input',
|
||||
label: '用户选择',
|
||||
helpMessage: ['插槽模式-自己保存查询条件'],
|
||||
slot: 'tabsSelectUser',
|
||||
colProps: { span: 14 },
|
||||
},
|
||||
{
|
||||
field: 'tabsSelectUser',
|
||||
component: 'JEllipsis',
|
||||
label: '选中值',
|
||||
colProps: { span: 10 },
|
||||
},
|
||||
{
|
||||
field: 'orderAuth',
|
||||
component: 'Input',
|
||||
@@ -952,5 +966,4 @@ export const schemas: FormSchema[] = [
|
||||
label: '选中值',
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
46
jeecgboot-vue3/src/views/demo/vextable/index3.vue
Normal file
46
jeecgboot-vue3/src/views/demo/vextable/index3.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<a-card :key="key">
|
||||
<div class="container">
|
||||
<p class="title">vxe-table 原生加载示例</p>
|
||||
<template v-if="isRegistered">
|
||||
<vxe-table :align="allAlign" :data="tableData1">
|
||||
<vxe-table-column type="seq" width="60"></vxe-table-column>
|
||||
<vxe-table-column field="name" title="Name"></vxe-table-column>
|
||||
<vxe-table-column field="sex" title="Sex"></vxe-table-column>
|
||||
<vxe-table-column field="age" title="Age"></vxe-table-column>
|
||||
</vxe-table>
|
||||
</template>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useVxeTableRegister } from '/@/components/jeecg/JVxeTable/useVxeTableRegister';
|
||||
const allAlign = ref<string | null>(null);
|
||||
const tableData1 = ref([
|
||||
{ id: 10001, name: 'Test1', role: 'Develop', sex: 'Man', age: 28, address: 'vxe-table 从入门到放弃' },
|
||||
{ id: 10002, name: 'Test2', role: 'Test', sex: 'Women', age: 22, address: 'Guangzhou' },
|
||||
{ id: 10003, name: 'Test3', role: 'PM', sex: 'Man', age: 32, address: 'Shanghai' },
|
||||
{ id: 10004, name: 'Test4', role: 'Designer', sex: 'Women', age: 24, address: 'Shanghai' },
|
||||
]);
|
||||
const key = ref(0);
|
||||
const isRegistered = ref(false);
|
||||
useVxeTableRegister().then(() => {
|
||||
console.log('useVxeTableRegister');
|
||||
isRegistered.value = true;
|
||||
// vxetable放在插槽中需要更新key才能重新渲染
|
||||
key.value++;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
padding: 5px;
|
||||
background-color: #fff;
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -77,13 +77,12 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, reactive, computed, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/src/components/Modal';
|
||||
import { JVxeTable } from '/src/components/jeecg/JVxeTable';
|
||||
import { columns, columns1 } from './jvxetable.data';
|
||||
import { orderCustomerList, orderTicketList, saveOrUpdate } from './jvxetable.api';
|
||||
import { useJvxeMethod } from '/@/hooks/system/useJvxeMethods.ts';
|
||||
export default defineComponent({
|
||||
name: 'JVexTableModal',
|
||||
components: { BasicModal, JVxeTable },
|
||||
components: { BasicModal },
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const tableH = ref(300);
|
||||
|
||||
@@ -1,153 +1,147 @@
|
||||
<!--用户选择框-->
|
||||
<template>
|
||||
<div>
|
||||
<BasicModal v-bind="$attrs" @register="register" title="数据对比" width="50%" destroyOnClose :showOkBtn="false">
|
||||
<a-row :gutter="6" v-if="dataVersionList" style="margin-left: 2px">
|
||||
<span style="margin-top: 5px; margin-right: 3px; margin-left: 4px">版本对比:</span>
|
||||
<a-select placeholder="版本号" @change="handleChange1" v-model:value="params.dataId1">
|
||||
<a-select-option v-for="(log, logindex) in dataVersionList" :key="log.value" :value="log.value">
|
||||
{{ log.text }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<BasicModal v-bind="$attrs" @register="register" title="数据版本对比" width="60%" destroyOnClose :showOkBtn="false">
|
||||
<!-- 版本选择区 -->
|
||||
<div class="compare-header">
|
||||
<div class="compare-header__info">
|
||||
<span class="compare-header__label">数据表:</span>
|
||||
<a-tag color="blue">{{ dataTable }}</a-tag>
|
||||
<span class="compare-header__label" style="margin-left: 16px">数据ID:</span>
|
||||
<span class="compare-header__id">{{ dataId }}</span>
|
||||
</div>
|
||||
<div class="compare-header__selector">
|
||||
<span class="compare-header__label">版本对比:</span>
|
||||
<a-select
|
||||
placeholder="选择版本"
|
||||
@change="handleChange1"
|
||||
v-model:value="params.dataId1"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option v-for="log in dataVersionList" :key="log.value" :value="log.value">
|
||||
V{{ log.text }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<span class="compare-header__vs">VS</span>
|
||||
<a-select
|
||||
placeholder="选择版本"
|
||||
@change="handleChange2"
|
||||
v-model:value="params.dataId2"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option v-for="log in dataVersionList" :key="log.value" :value="log.value">
|
||||
V{{ log.text }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-select placeholder="版本号" @change="handleChange2" style="padding-left: 10px" v-model:value="params.dataId2">
|
||||
<a-select-option v-for="(log, logindex) in dataVersionList" :key="log.value" :value="log.value">
|
||||
{{ log.text }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-row>
|
||||
<BasicTable
|
||||
:columns="columns"
|
||||
v-bind="getBindValue"
|
||||
:rowClassName="setDataCss"
|
||||
:striped="false"
|
||||
:showIndexColumn="false"
|
||||
:pagination="false"
|
||||
:canResize="false"
|
||||
:bordered="true"
|
||||
:dataSource="dataSource"
|
||||
:searchInfo="searchInfo"
|
||||
v-if="isUpdate"
|
||||
>
|
||||
<template #dataVersionTitle1="{ record }"> <Icon icon="icon-park-outline:grinning-face" /> 版本:{{ dataVersion1Num }} </template>
|
||||
<template #dataVersionTitle2="{ record }"> <Icon icon="icon-park-outline:grinning-face" /> 版本:{{ dataVersion2Num }} </template>
|
||||
<template #avatarslot="{ record }">
|
||||
<div class="anty-img-wrap" v-if="record.dataVersion1 != record.dataVersion2">
|
||||
<Icon icon="mdi:arrow-right-bold" style="color: red"></Icon>
|
||||
</div>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<!-- 差异统计 -->
|
||||
<div class="compare-stats" v-if="dataSource.length > 0">
|
||||
<a-tag color="red">{{ diffCount }} 处差异</a-tag>
|
||||
<a-tag color="green">{{ dataSource.length - diffCount }} 处相同</a-tag>
|
||||
<span class="compare-stats__total">共 {{ dataSource.length }} 个字段</span>
|
||||
</div>
|
||||
|
||||
<!-- 对比表格 -->
|
||||
<div class="compare-table" v-if="isUpdate">
|
||||
<table class="compare-table__inner">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-field">字段名</th>
|
||||
<th class="col-value">
|
||||
<span class="version-tag version-tag--left">V{{ dataVersion1Num }}</span>
|
||||
</th>
|
||||
<th class="col-status"></th>
|
||||
<th class="col-value">
|
||||
<span class="version-tag version-tag--right">V{{ dataVersion2Num }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, idx) in dataSource" :key="idx" :class="{ 'row-diff': row.isDiff, 'row-same': !row.isDiff }">
|
||||
<td class="col-field">
|
||||
<span class="field-name">{{ row.code }}</span>
|
||||
</td>
|
||||
<td class="col-value" :class="{ 'cell-diff': row.isDiff }">
|
||||
<span class="cell-text">{{ formatValue(row.dataVersion1) }}</span>
|
||||
</td>
|
||||
<td class="col-status">
|
||||
<span v-if="row.isDiff" class="diff-icon">≠</span>
|
||||
<span v-else class="same-icon">=</span>
|
||||
</td>
|
||||
<td class="col-value" :class="{ 'cell-diff': row.isDiff }">
|
||||
<span class="cell-text">{{ formatValue(row.dataVersion2) }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, unref, ref, reactive, watch } from 'vue';
|
||||
import { defineComponent, unref, ref, reactive, computed } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { queryCompareList, queryDataVerList } from './datalog.api';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { selectProps } from '/@/components/Form/src/jeecg/props/props';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DataLogCompareModal',
|
||||
components: {
|
||||
//此处需要异步加载BasicTable
|
||||
BasicModal,
|
||||
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), { loading: true }),
|
||||
},
|
||||
props: {
|
||||
...selectProps,
|
||||
},
|
||||
emits: ['register', 'btnOk'],
|
||||
setup(props, { emit, refs }) {
|
||||
setup() {
|
||||
const { createMessage } = useMessage();
|
||||
const attrs = useAttrs();
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs));
|
||||
const dataSource = ref([]);
|
||||
const dataSource = ref<any[]>([]);
|
||||
const dataVersion1Num = ref('');
|
||||
const dataVersion2Num = ref('');
|
||||
const isUpdate = ref(true);
|
||||
const searchInfo = {};
|
||||
const dataId1 = ref('');
|
||||
const dataId2 = ref('');
|
||||
const dataId = ref('');
|
||||
const dataTable1 = ref('');
|
||||
const dataID3 = ref('');
|
||||
const dataTable = ref('');
|
||||
const confirmLoading = ref(false);
|
||||
const dataVersionList = ref([]);
|
||||
let params = reactive({ dataId1: '', dataId2: '' });
|
||||
let dataLog = reactive({});
|
||||
const [register, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
const dataVersionList = ref<any[]>([]);
|
||||
const params = reactive({ dataId1: '', dataId2: '' });
|
||||
|
||||
const diffCount = computed(() => dataSource.value.filter((r) => r.isDiff).length);
|
||||
|
||||
const [register, { setModalProps }] = useModalInner(async (data) => {
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
if (unref(isUpdate)) {
|
||||
let checkedRows = data.selectedRows;
|
||||
const checkedRows = data.selectedRows;
|
||||
dataTable.value = checkedRows[0].dataTable;
|
||||
dataId.value = checkedRows[0].dataId;
|
||||
dataId1.value = checkedRows[0].id;
|
||||
dataId2.value = checkedRows[1].id;
|
||||
params.dataId1 = dataId1.value;
|
||||
params.dataId2 = dataId2.value;
|
||||
params.dataId1 = checkedRows[0].id;
|
||||
params.dataId2 = checkedRows[1].id;
|
||||
await initDataVersionList();
|
||||
await initTableData();
|
||||
}
|
||||
});
|
||||
|
||||
//定义表格列
|
||||
const columns = [
|
||||
{
|
||||
title: '字段名',
|
||||
dataIndex: 'code',
|
||||
width: 20,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
dataIndex: 'dataVersion1',
|
||||
align: 'left',
|
||||
width: 60,
|
||||
slots: { title: 'dataVersionTitle1' },
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'imgshow',
|
||||
align: 'center',
|
||||
slots: { customRender: 'avatarslot' },
|
||||
width: 5,
|
||||
},
|
||||
{
|
||||
align: 'left',
|
||||
dataIndex: 'dataVersion2',
|
||||
width: 60,
|
||||
filters: [],
|
||||
filterMultiple: false,
|
||||
slots: { title: 'dataVersionTitle2' },
|
||||
},
|
||||
];
|
||||
async function initTableData() {
|
||||
console.info('params', params);
|
||||
queryCompareList(unref(params)).then((res) => {
|
||||
console.info('test', res);
|
||||
dataVersion1Num.value = res[0].dataVersion;
|
||||
dataVersion2Num.value = res[1].dataVersion;
|
||||
let json1 = JSON.parse(res[0].dataContent);
|
||||
let json2 = JSON.parse(res[1].dataContent);
|
||||
let data = [];
|
||||
for (var item1 in json1) {
|
||||
for (var item2 in json2) {
|
||||
if (item1 == item2) {
|
||||
data.push({
|
||||
code: item1,
|
||||
imgshow: '',
|
||||
dataVersion1: json1[item1],
|
||||
dataVersion2: json2[item2],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const json1 = JSON.parse(res[0].dataContent);
|
||||
const json2 = JSON.parse(res[1].dataContent);
|
||||
// 收集所有字段(兼顾两边都有的和只有一边有的)
|
||||
const allKeys = new Set([...Object.keys(json1), ...Object.keys(json2)]);
|
||||
const data: any[] = [];
|
||||
allKeys.forEach((fieldKey) => {
|
||||
const v1 = json1[fieldKey] ?? '';
|
||||
const v2 = json2[fieldKey] ?? '';
|
||||
data.push({
|
||||
code: fieldKey,
|
||||
dataVersion1: v1,
|
||||
dataVersion2: v2,
|
||||
isDiff: String(v1) !== String(v2),
|
||||
});
|
||||
});
|
||||
// 差异项排前面
|
||||
data.sort((a, b) => (a.isDiff === b.isDiff ? 0 : a.isDiff ? -1 : 1));
|
||||
dataSource.value = data;
|
||||
});
|
||||
}
|
||||
|
||||
function handleChange1(value) {
|
||||
if (params.dataId2 == value) {
|
||||
createMessage.warning('相同版本号不能比较');
|
||||
@@ -156,6 +150,7 @@
|
||||
params.dataId1 = value;
|
||||
initTableData();
|
||||
}
|
||||
|
||||
function handleChange2(value) {
|
||||
if (params.dataId1 == value) {
|
||||
createMessage.warning('相同版本号不能比较');
|
||||
@@ -164,57 +159,223 @@
|
||||
params.dataId2 = value;
|
||||
initTableData();
|
||||
}
|
||||
function setDataCss(record) {
|
||||
let className = 'trcolor';
|
||||
const dataVersion1 = record.dataVersion1;
|
||||
const dataVersion2 = record.dataVersion2;
|
||||
if (dataVersion1 != dataVersion2) {
|
||||
return className;
|
||||
}
|
||||
}
|
||||
|
||||
async function initDataVersionList() {
|
||||
queryDataVerList({ dataTable: dataTable.value, dataId: dataId.value }).then((res) => {
|
||||
dataVersionList.value = res.map((value, key, arr) => {
|
||||
let item = {};
|
||||
item['text'] = value['dataVersion'];
|
||||
item['value'] = value['id'];
|
||||
return item;
|
||||
});
|
||||
dataVersionList.value = res.map((value) => ({
|
||||
text: value['dataVersion'],
|
||||
value: value['id'],
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function formatValue(val) {
|
||||
if (val === null || val === undefined || val === '') return '--';
|
||||
return String(val);
|
||||
}
|
||||
|
||||
return {
|
||||
//config,
|
||||
searchInfo,
|
||||
dataSource,
|
||||
setDataCss,
|
||||
isUpdate,
|
||||
dataVersionList,
|
||||
dataVersion1Num,
|
||||
dataVersion2Num,
|
||||
queryCompareList,
|
||||
initDataVersionList,
|
||||
register,
|
||||
handleChange1,
|
||||
handleChange2,
|
||||
params,
|
||||
getBindValue,
|
||||
columns,
|
||||
dataTable,
|
||||
dataId,
|
||||
diffCount,
|
||||
formatValue,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.anty-img-wrap {
|
||||
height: 25px;
|
||||
position: relative;
|
||||
<style lang="less" scoped>
|
||||
.compare-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__id {
|
||||
font-size: 12px;
|
||||
color: #595959;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__vs {
|
||||
font-weight: 600;
|
||||
color: #faad14;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.anty-img-wrap > img {
|
||||
max-height: 100%;
|
||||
.compare-stats {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&__total {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.marginCss {
|
||||
margin-top: 20px;
|
||||
.compare-table {
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
&__inner {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
|
||||
thead {
|
||||
tr {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 10px 12px;
|
||||
font-weight: 500;
|
||||
color: #595959;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
&:not(:last-child) td {
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
color: #333;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.col-field {
|
||||
width: 140px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.col-value {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.col-status {
|
||||
width: 36px;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.cell-text {
|
||||
word-break: break-all;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.row-diff {
|
||||
.field-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-diff {
|
||||
background: #fff7e6;
|
||||
|
||||
.cell-text {
|
||||
color: #d46b08;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.diff-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #fff1f0;
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.same-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&--left {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&--right {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,31 +1,80 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
import { h } from 'vue';
|
||||
import { Tag, Tooltip } from 'ant-design-vue';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: '表名',
|
||||
dataIndex: 'dataTable',
|
||||
width: 150,
|
||||
width: 120,
|
||||
align: 'left',
|
||||
customRender: ({ text }) => {
|
||||
return h(Tag, { color: 'blue' }, () => text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '数据ID',
|
||||
dataIndex: 'dataId',
|
||||
width: 350,
|
||||
width: 260,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => {
|
||||
return h(
|
||||
'span',
|
||||
{ style: 'font-family: Consolas, Monaco, monospace; font-size: 12px; color: #595959' },
|
||||
text
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '版本号',
|
||||
dataIndex: 'dataVersion',
|
||||
width: 100,
|
||||
width: 70,
|
||||
align: 'center',
|
||||
customRender: ({ text }) => {
|
||||
return h(Tag, { color: 'green' }, () => 'V' + text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '数据内容',
|
||||
dataIndex: 'dataContent',
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => {
|
||||
if (!text) return '--';
|
||||
// 尝试格式化 JSON 显示关键字段
|
||||
try {
|
||||
const obj = JSON.parse(text);
|
||||
const keys = Object.keys(obj);
|
||||
const preview = keys
|
||||
.slice(0, 3)
|
||||
.map((k) => {
|
||||
const v = obj[k];
|
||||
const val = v === null || v === undefined || v === '' ? '--' : String(v);
|
||||
return `${k}: ${val.length > 20 ? val.substring(0, 20) + '...' : val}`;
|
||||
})
|
||||
.join(' | ');
|
||||
const suffix = keys.length > 3 ? ` (+${keys.length - 3} 字段)` : '';
|
||||
return h(
|
||||
Tooltip,
|
||||
{ title: JSON.stringify(obj, null, 2), overlayStyle: { maxWidth: '500px', whiteSpace: 'pre-wrap', fontFamily: 'Consolas, monospace', fontSize: '12px' } },
|
||||
() => h('span', { style: 'font-size: 12px; color: #595959' }, preview + suffix)
|
||||
);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: 'createBy',
|
||||
sorter: true,
|
||||
width: 200,
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -34,12 +83,27 @@ export const searchFormSchema: FormSchema[] = [
|
||||
field: 'dataTable',
|
||||
label: '表名',
|
||||
component: 'Input',
|
||||
colProps: { span: 8 },
|
||||
componentProps: {
|
||||
placeholder: '请输入表名',
|
||||
},
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
field: 'dataId',
|
||||
label: '数据ID',
|
||||
component: 'Input',
|
||||
colProps: { span: 8 },
|
||||
componentProps: {
|
||||
placeholder: '请输入数据ID',
|
||||
},
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
field: 'createBy',
|
||||
label: '创建人',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入创建人',
|
||||
},
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,34 +2,38 @@
|
||||
<div>
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-button preIcon="ant-design:plus-outlined" type="primary" @click="handleCompare" style="margin-right: 5px">数据比较</a-button>
|
||||
<a-button preIcon="ant-design:swap-outlined" type="primary" size="small" @click="handleCompare">数据比较</a-button>
|
||||
<span v-if="selectedRowKeys.length === 0" class="compare-tip">请勾选两条相同数据ID的记录</span>
|
||||
<a-tag v-else-if="selectedRowKeys.length === 1" color="warning" style="margin-left: 10px">再选一条相同数据ID的记录</a-tag>
|
||||
<a-tag v-else-if="selectedRowKeys.length === 2 && !isSameDataId" color="error" style="margin-left: 10px">数据ID不一致,无法比较</a-tag>
|
||||
<a-tag v-else-if="selectedRowKeys.length === 2 && isSameDataId" color="success" style="margin-left: 10px">可以比较 V{{ selectedRows[0]?.dataVersion }} vs V{{ selectedRows[1]?.dataVersion }}</a-tag>
|
||||
<a-tag v-else color="error" style="margin-left: 10px">只能选择两条记录</a-tag>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<DataLogCompareModal @register="registerModal" @success="reload" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" name="monitor-datalog" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { computed } from 'vue';
|
||||
import { BasicTable } from '/@/components/Table';
|
||||
import DataLogCompareModal from './DataLogCompareModal.vue';
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
import { getDataLogList } from './datalog.api';
|
||||
import { columns, searchFormSchema } from './datalog.data';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
const { createMessage } = useMessage();
|
||||
const checkedRows = ref<Array<object | number>>([]);
|
||||
|
||||
// 列表页面公共参数、方法
|
||||
const { prefixCls, tableContext } = useListPage({
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const { tableContext } = useListPage({
|
||||
designScope: 'datalog-template',
|
||||
tableProps: {
|
||||
title: '数据日志列表',
|
||||
api: getDataLogList,
|
||||
columns: columns,
|
||||
formConfig: {
|
||||
labelWidth: 120,
|
||||
labelWidth: 80,
|
||||
schemas: searchFormSchema,
|
||||
},
|
||||
actionColumn: false,
|
||||
@@ -38,20 +42,32 @@
|
||||
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
|
||||
|
||||
const isSameDataId = computed(() => {
|
||||
const rows = selectedRows.value;
|
||||
if (!rows || rows.length !== 2) return false;
|
||||
return rows[0].dataId === rows[1].dataId;
|
||||
});
|
||||
|
||||
function handleCompare() {
|
||||
let obj = selectedRows.value;
|
||||
console.info('sfsfsf', obj);
|
||||
if (!obj || obj.length != 2) {
|
||||
createMessage.warning('请选择两条数据!');
|
||||
return false;
|
||||
const rows = selectedRows.value;
|
||||
if (!rows || rows.length !== 2) {
|
||||
createMessage.warning('请选择两条数据进行比较!');
|
||||
return;
|
||||
}
|
||||
if (obj[0].dataId != obj[1].dataId) {
|
||||
createMessage.warning('请选择相同的数据库表和数据ID进行比较!');
|
||||
return false;
|
||||
if (rows[0].dataId !== rows[1].dataId) {
|
||||
createMessage.warning('请选择相同数据ID的记录进行比较!');
|
||||
return;
|
||||
}
|
||||
openModal(true, {
|
||||
selectedRows,
|
||||
selectedRows: rows,
|
||||
isUpdate: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.compare-tip {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
>
|
||||
</div>
|
||||
<div v-if="searchInfo.logType == 4">
|
||||
<div style="margin-bottom: 5px">
|
||||
<a-badge status="success" style="vertical-align: middle" />
|
||||
<span class="error-box" style="vertical-align: middle">异常堆栈:{{ record.requestParam }}</span>
|
||||
<div class="error-section">
|
||||
<div class="error-label"><a-badge status="error" /> 异常堆栈:</div>
|
||||
<pre class="error-box">{{ record.requestParam }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -105,19 +105,42 @@
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.error-box {
|
||||
white-space: break-spaces;
|
||||
.error-section {
|
||||
.error-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.error-box {
|
||||
margin: 0;
|
||||
padding: 12px 16px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.table-title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
:deep(.jeecg-basic-table-header__toolbar){
|
||||
width:100px !important;
|
||||
|
||||
:deep(.jeecg-basic-table-header__toolbar) {
|
||||
width: 100px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,8 +4,9 @@ export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: '日志内容',
|
||||
dataIndex: 'logContent',
|
||||
width: 100,
|
||||
width: 200,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作人ID',
|
||||
@@ -20,12 +21,12 @@ export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: 'IP',
|
||||
dataIndex: 'ip',
|
||||
width: 80,
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
title: '耗时(毫秒)',
|
||||
dataIndex: 'costTime',
|
||||
width: 80,
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
@@ -36,7 +37,7 @@ export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: '客户端类型',
|
||||
dataIndex: 'clientType_dictText',
|
||||
width: 60,
|
||||
width: 50,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -56,30 +57,35 @@ export const exceptionColumns: BasicColumn[] = [
|
||||
{
|
||||
title: '异常标题',
|
||||
dataIndex: 'logContent',
|
||||
width: 100,
|
||||
width: 200,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '请求地址',
|
||||
dataIndex: 'requestUrl',
|
||||
width: 100,
|
||||
width: 140,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '请求参数',
|
||||
title: '请求方法',
|
||||
dataIndex: 'method',
|
||||
width: 60,
|
||||
width: 120,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'username',
|
||||
width: 60,
|
||||
width: 80,
|
||||
customRender: ({ record }) => {
|
||||
let pname = record.username;
|
||||
let pid = record.userid;
|
||||
if(!pname && !pid){
|
||||
return "";
|
||||
const pname = record.username;
|
||||
const pid = record.userid;
|
||||
if (!pname && !pid) {
|
||||
return '';
|
||||
}
|
||||
return pname + " (账号: "+ pid + " )";
|
||||
return pname + ' (' + pid + ')';
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -91,12 +97,12 @@ export const exceptionColumns: BasicColumn[] = [
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
sorter: true,
|
||||
width: 60,
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '客户端类型',
|
||||
dataIndex: 'clientType_dictText',
|
||||
width: 60,
|
||||
width: 50,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</template>
|
||||
</a-card-meta>
|
||||
<a-divider />
|
||||
<div v-html="content.msgContent" class="article-content"></div>
|
||||
<div v-html="removeSpecialTags(content.msgContent)" class="article-content"></div>
|
||||
<div>
|
||||
<a-button v-if="hasHref" @click="jumpToHandlePage">前往办理<ArrowRightOutlined /></a-button>
|
||||
</div>
|
||||
@@ -81,6 +81,7 @@
|
||||
import { getToken } from '@/utils/auth';
|
||||
import {defHttp} from "@/utils/http/axios";
|
||||
import {$electron} from "@/electron";
|
||||
import { removeSpecialTags } from '@/utils/index';
|
||||
const router = useRouter();
|
||||
const glob = useGlobSetting();
|
||||
const isUpdate = ref(true);
|
||||
@@ -279,7 +280,9 @@
|
||||
function handleViewFile(filePath) {
|
||||
if (filePath) {
|
||||
console.log('glob.onlineUrl', glob.viewUrl);
|
||||
let url = encodeURIComponent(encryptByBase64(filePath));
|
||||
//update-begin-author:scott---date:2026-04-16--for: 【Github #8855】修复文件预览路径处理问题,filePath需要先拼接完整URL再编码
|
||||
let url = encodeURIComponent(encryptByBase64(getFileAccessHttpUrl(filePath)));
|
||||
//update-end-author:scott---date:2026-04-16--for: 【Github #8855】修复文件预览路径处理问题,filePath需要先拼接完整URL再编码
|
||||
let previewUrl = `${glob.viewUrl}?url=` + url;
|
||||
//update-begin-author:liusq---date:2025-12-16--for: JHHB-1139桌面端 文件预览统一修改
|
||||
if($electron.isElectron()){
|
||||
@@ -377,6 +380,20 @@
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
/* 修复 Word 复制内容中表格边框丢失和间隔问题 */
|
||||
.article-content {
|
||||
:deep(table) {
|
||||
border-collapse: collapse !important;
|
||||
border-spacing: 0 !important;
|
||||
}
|
||||
:deep(table td),
|
||||
:deep(table th) {
|
||||
border: 1px solid #d0d0d0;
|
||||
padding: 4px 8px;
|
||||
min-width: 20px;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
.basic-title{
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
@@ -19,10 +19,10 @@ export const options = {
|
||||
a: ['style', 'target', 'href', 'title', 'rel'],
|
||||
img: ['style', 'src', 'title','width','height'],
|
||||
div: ['style'],
|
||||
table: ['style', 'width', 'border', 'height'],
|
||||
tr: ['style'],
|
||||
td: ['style', 'width', 'colspan'],
|
||||
th: ['style', 'width', 'colspan'],
|
||||
table: ['style', 'width', 'border', 'height', 'cellspacing', 'cellpadding'],
|
||||
tr: ['style', 'valign', 'align'],
|
||||
td: ['style', 'width', 'colspan', 'rowspan', 'border', 'valign', 'align'],
|
||||
th: ['style', 'width', 'colspan', 'rowspan', 'border', 'valign', 'align'],
|
||||
tbody: ['style'],
|
||||
ul: ['style'],
|
||||
li: ['style'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
import { render } from '/@/utils/common/renderUtils';
|
||||
import { JCronValidator } from '/@/components/Form';
|
||||
import JCronValidator from '/@/components/Form/src/jeecg/components/JEasyCron/validator';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{
|
||||
|
||||
@@ -1,130 +1,188 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<a-card>
|
||||
<!-- Redis 信息实时监控 -->
|
||||
<a-row :gutter="8">
|
||||
<a-col :sm="24" :xl="12">
|
||||
<div ref="chartRef" style="width: 100%; height: 300px"></div>
|
||||
</a-col>
|
||||
<a-col :sm="24" :xl="12">
|
||||
<div ref="chartRef2" style="width: 100%; height: 300px"></div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
<div class="redis-monitor p-4">
|
||||
<!-- 顶部概览卡片 -->
|
||||
<a-row :gutter="16" class="overview-row">
|
||||
<a-col :sm="12" :md="6" v-for="item in overviewCards" :key="item.label">
|
||||
<div class="overview-card" :style="{ borderTopColor: item.color }">
|
||||
<div class="overview-card__value" :style="{ color: item.color }">{{ item.value }}</div>
|
||||
<div class="overview-card__label">{{ item.label }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<BasicTable @register="registerTable" :api="getInfo"></BasicTable>
|
||||
<!-- Redis 信息实时监控 -->
|
||||
<a-row :gutter="16" class="chart-row">
|
||||
<a-col :sm="24" :xl="12">
|
||||
<a-card :bordered="false" class="chart-card">
|
||||
<div ref="chartRef" style="width: 100%; height: 300px"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :xl="12">
|
||||
<a-card :bordered="false" class="chart-card">
|
||||
<div ref="chartRef2" style="width: 100%; height: 300px"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- Redis 详细信息表格 -->
|
||||
<a-card :bordered="false" class="table-card" title="Redis 配置详情">
|
||||
<BasicTable @register="registerTable" :api="getInfo" :canResize="false"></BasicTable>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" name="monitor-redis" setup>
|
||||
import { onMounted, ref, reactive, Ref, onUnmounted } from 'vue';
|
||||
import { BasicTable, useTable, TableAction } from '/@/components/Table';
|
||||
import { onMounted, ref, reactive, Ref, onUnmounted, computed } from 'vue';
|
||||
import { BasicTable, useTable } from '/@/components/Table';
|
||||
import { getInfo, getRedisInfo, getMetricsHistory } from './redis.api';
|
||||
import dayjs from 'dayjs';
|
||||
import { columns } from './redis.data';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useECharts } from '/@/hooks/web/useECharts';
|
||||
|
||||
const dataSource = ref([]);
|
||||
const chartRef = ref<HTMLDivElement | null>(null);
|
||||
const chartRef2 = ref<HTMLDivElement | null>(null);
|
||||
const { setOptions, echarts } = useECharts(chartRef as Ref<HTMLDivElement>);
|
||||
const { setOptions: setOptions2, echarts: echarts2 } = useECharts(chartRef2 as Ref<HTMLDivElement>);
|
||||
const loading = ref(false);
|
||||
let timer = null;
|
||||
const { createMessage } = useMessage();
|
||||
const key = reactive({
|
||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
|
||||
const { setOptions: setOptions2 } = useECharts(chartRef2 as Ref<HTMLDivElement>);
|
||||
let timer: any = null;
|
||||
|
||||
// 概览数据
|
||||
const currentMemory = ref('--');
|
||||
const currentKeys = ref('--');
|
||||
const currentUptime = ref('--');
|
||||
const currentPort = ref('--');
|
||||
|
||||
const overviewCards = computed(() => [
|
||||
{ label: '已用内存', value: currentMemory.value, color: '#1890ff' },
|
||||
{ label: 'Key 数量', value: currentKeys.value, color: '#52c41a' },
|
||||
{ label: '运行时间', value: currentUptime.value, color: '#faad14' },
|
||||
{ label: '监听端口', value: currentPort.value, color: '#722ed1' },
|
||||
]);
|
||||
|
||||
const memoryOption = reactive({
|
||||
title: {
|
||||
text: 'Redis Key 实时数量(个)',
|
||||
text: 'Redis 内存实时占用(KB)',
|
||||
textStyle: { fontSize: 14, fontWeight: 500, color: '#333' },
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [],
|
||||
type: 'line',
|
||||
areaStyle: {
|
||||
color: '#ff6987',
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#dc143c',
|
||||
width: 10,
|
||||
type: 'solid',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(255,255,255,0.96)',
|
||||
borderColor: '#e8e8e8',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#333', fontSize: 12 },
|
||||
formatter(params) {
|
||||
const p = params[0];
|
||||
return `<div style="font-weight:500;margin-bottom:4px">${p.axisValue}</div>
|
||||
<span style="color:#1890ff">● 内存</span>:${p.value} KB`;
|
||||
},
|
||||
],
|
||||
});
|
||||
const memory = reactive({
|
||||
title: {
|
||||
text: 'Redis 内存实时占用情况(KB)',
|
||||
},
|
||||
grid: { top: 50, right: 20, bottom: 30, left: 60 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [],
|
||||
axisLine: { lineStyle: { color: '#d9d9d9' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#8c8c8c', fontSize: 11 },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#8c8c8c', fontSize: 11 },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
showSymbol: false,
|
||||
lineStyle: { color: '#1890ff', width: 2 },
|
||||
itemStyle: { color: '#1890ff' },
|
||||
areaStyle: {
|
||||
color: '#74bcff',
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#1890ff',
|
||||
width: 10,
|
||||
type: 'solid',
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(24,144,255,0.25)' },
|
||||
{ offset: 1, color: 'rgba(24,144,255,0.02)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [registerTable, { reload }] = useTable({
|
||||
const keyOption = reactive({
|
||||
title: {
|
||||
text: 'Redis Key 实时数量(个)',
|
||||
textStyle: { fontSize: 14, fontWeight: 500, color: '#333' },
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(255,255,255,0.96)',
|
||||
borderColor: '#e8e8e8',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#333', fontSize: 12 },
|
||||
formatter(params) {
|
||||
const p = params[0];
|
||||
return `<div style="font-weight:500;margin-bottom:4px">${p.axisValue}</div>
|
||||
<span style="color:#52c41a">● Key 数量</span>:${p.value}`;
|
||||
},
|
||||
},
|
||||
grid: { top: 50, right: 20, bottom: 30, left: 60 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [],
|
||||
axisLine: { lineStyle: { color: '#d9d9d9' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#8c8c8c', fontSize: 11 },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#8c8c8c', fontSize: 11 },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
showSymbol: false,
|
||||
lineStyle: { color: '#52c41a', width: 2 },
|
||||
itemStyle: { color: '#52c41a' },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(82,196,26,0.25)' },
|
||||
{ offset: 1, color: 'rgba(82,196,26,0.02)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [registerTable] = useTable({
|
||||
columns,
|
||||
showIndexColumn: false,
|
||||
pagination: false,
|
||||
bordered: true,
|
||||
canResize: false,
|
||||
showTableSetting: false,
|
||||
});
|
||||
|
||||
// 获取一组数据中最大和最小的值
|
||||
function getMaxAndMin(dataSource, field) {
|
||||
let maxValue = null,
|
||||
minValue = null;
|
||||
dataSource.forEach((item) => {
|
||||
let value = Number.parseInt(item[field]);
|
||||
// max
|
||||
if (maxValue == null) {
|
||||
maxValue = value;
|
||||
} else if (value > maxValue) {
|
||||
maxValue = value;
|
||||
}
|
||||
// min
|
||||
if (minValue == null) {
|
||||
minValue = value;
|
||||
} else if (value < minValue) {
|
||||
minValue = value;
|
||||
}
|
||||
});
|
||||
return [maxValue, minValue];
|
||||
}
|
||||
|
||||
function loadRedisInfo() {
|
||||
getInfo().then((res) => {
|
||||
dataSource.value = res.result;
|
||||
});
|
||||
}
|
||||
|
||||
function initCharts() {
|
||||
setOptions(memory);
|
||||
setOptions2(key);
|
||||
setOptions(memoryOption);
|
||||
setOptions2(keyOption);
|
||||
}
|
||||
|
||||
/** 开启定时器 */
|
||||
@@ -141,70 +199,127 @@
|
||||
if (timer) clearInterval(timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载历史监控数据
|
||||
*/
|
||||
/** 加载历史监控数据 */
|
||||
function loadHistoryData() {
|
||||
getMetricsHistory().then((res) => {
|
||||
let dbSizes = res.dbSize;
|
||||
let memories = res.memory;
|
||||
const dbSizes = res.dbSize;
|
||||
const memories = res.memory;
|
||||
dbSizes.forEach((dbSize) => {
|
||||
key.xAxis.data.push(dayjs(dbSize.create_time).format('hh:mm:ss'));
|
||||
key.series[0].data.push(dbSize.dbSize);
|
||||
keyOption.xAxis.data.push(dayjs(dbSize.create_time).format('HH:mm:ss'));
|
||||
keyOption.series[0].data.push(dbSize.dbSize);
|
||||
});
|
||||
memories.forEach((memoryData) => {
|
||||
memory.xAxis.data.push(dayjs(memoryData.create_time).format('hh:mm:ss'));
|
||||
memory.series[0].data.push(memoryData.used_memory / 1000);
|
||||
memoryOption.xAxis.data.push(dayjs(memoryData.create_time).format('HH:mm:ss'));
|
||||
memoryOption.series[0].data.push(memoryData.used_memory / 1000);
|
||||
});
|
||||
setOptions(memory, false);
|
||||
setOptions2(key, false);
|
||||
// 更新概览卡片
|
||||
if (memories.length > 0) {
|
||||
const lastMem = memories[memories.length - 1].used_memory / 1000;
|
||||
currentMemory.value = lastMem.toFixed(0) + ' KB';
|
||||
}
|
||||
if (dbSizes.length > 0) {
|
||||
currentKeys.value = dbSizes[dbSizes.length - 1].dbSize + '';
|
||||
}
|
||||
setOptions(memoryOption, false);
|
||||
setOptions2(keyOption, false);
|
||||
});
|
||||
// 加载详细信息获取端口和运行时间
|
||||
getInfo().then((res) => {
|
||||
const list = res.result || res;
|
||||
if (Array.isArray(list)) {
|
||||
list.forEach((item) => {
|
||||
if (item.key === 'tcp_port') currentPort.value = item.value;
|
||||
if (item.key === 'uptime_in_days') currentUptime.value = item.value + ' 天';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
getRedisInfo()
|
||||
.then((res) => {
|
||||
let time = dayjs().format('hh:mm:ss');
|
||||
let [{ dbSize: currentSize }, memoryInfo] = res;
|
||||
let currentMemory = memoryInfo.used_memory / 1000;
|
||||
// push 数据
|
||||
key.xAxis.data.push(time);
|
||||
key.series[0].data.push(currentSize);
|
||||
memory.xAxis.data.push(time);
|
||||
memory.series[0].data.push(currentMemory);
|
||||
const time = dayjs().format('HH:mm:ss');
|
||||
const [{ dbSize: curSize }, memInfo] = res;
|
||||
const curMem = memInfo.used_memory / 1000;
|
||||
|
||||
keyOption.xAxis.data.push(time);
|
||||
keyOption.series[0].data.push(curSize);
|
||||
memoryOption.xAxis.data.push(time);
|
||||
memoryOption.series[0].data.push(curMem);
|
||||
|
||||
// 更新概览
|
||||
currentMemory.value = curMem.toFixed(0) + ' KB';
|
||||
currentKeys.value = curSize + '';
|
||||
|
||||
// 最大长度为80
|
||||
if (key.series[0].data.length > 80) {
|
||||
key.xAxis.data.splice(0, 1);
|
||||
key.series[0].data.splice(0, 1);
|
||||
memory.xAxis.data.splice(0, 1);
|
||||
memory.series[0].data.splice(0, 1);
|
||||
if (keyOption.series[0].data.length > 80) {
|
||||
keyOption.xAxis.data.splice(0, 1);
|
||||
keyOption.series[0].data.splice(0, 1);
|
||||
memoryOption.xAxis.data.splice(0, 1);
|
||||
memoryOption.series[0].data.splice(0, 1);
|
||||
}
|
||||
setOptions(memory, false);
|
||||
setOptions2(key, false);
|
||||
|
||||
// 计算 Key 最大最小值
|
||||
//let keyPole = getMaxAndMin(key.dataSource, 'y');
|
||||
//key.max = Math.floor(keyPole[0]) + 10;
|
||||
//key.min = Math.floor(keyPole[1]) - 10;
|
||||
//if (key.min < 0) this.key.min = 0;
|
||||
|
||||
// 计算 Memory 最大最小值
|
||||
//let memoryPole = getMaxAndMin(memory.dataSource, 'y');
|
||||
//memory.max = Math.floor(memoryPole[0]) + 100;
|
||||
//memory.min = Math.floor(memoryPole[1]) - 100;
|
||||
//if (memory.min < 0) memory.min = 0;
|
||||
setOptions(memoryOption, false);
|
||||
setOptions2(keyOption, false);
|
||||
})
|
||||
.catch((e) => {
|
||||
//closeTimer()
|
||||
});
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCharts();
|
||||
openTimer();
|
||||
});
|
||||
// 代码逻辑说明: 【issues-615】系统监控中的REDIS监控页面打开,再关闭后,没有关闭计时器
|
||||
|
||||
onUnmounted(() => {
|
||||
closeTimer();
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.redis-monitor {
|
||||
.overview-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 20px 24px;
|
||||
border-top: 3px solid #1890ff;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-card {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,18 +2,25 @@ import { BasicColumn } from '/@/components/Table';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: 'Key',
|
||||
title: '配置项',
|
||||
dataIndex: 'key',
|
||||
width: 100,
|
||||
width: 120,
|
||||
align: 'left',
|
||||
customRender: ({ text }) => {
|
||||
return text;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
title: '说明',
|
||||
dataIndex: 'description',
|
||||
width: 80,
|
||||
width: 200,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
title: '值',
|
||||
dataIndex: 'value',
|
||||
width: 80,
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -11,35 +11,34 @@ export const columns: BasicColumn[] = [
|
||||
align:"center",
|
||||
dataIndex: 'name'
|
||||
},
|
||||
{
|
||||
title: '请求方法',
|
||||
align:"center",
|
||||
dataIndex: 'requestMethod'
|
||||
},
|
||||
{
|
||||
title: '接口地址',
|
||||
align:"center",
|
||||
dataIndex: 'requestUrl'
|
||||
dataIndex: 'requestUrl',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: 'IP 黑名单',
|
||||
title: '请求方式',
|
||||
align:"center",
|
||||
dataIndex: 'blackList'
|
||||
},
|
||||
// {
|
||||
// title: '状态',
|
||||
// align:"center",
|
||||
// dataIndex: 'status'
|
||||
// },
|
||||
{
|
||||
title: '创建人',
|
||||
align:"center",
|
||||
dataIndex: 'createBy'
|
||||
dataIndex: 'requestMethod',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
title: '原始接口',
|
||||
align:"center",
|
||||
dataIndex: 'createTime'
|
||||
dataIndex: 'originUrl',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'IP 白名单',
|
||||
align:"center",
|
||||
dataIndex: 'whiteList',
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => {
|
||||
if (!text) return '不限制';
|
||||
const count = text.split(/[,\n]/).filter(item => item.trim()).length;
|
||||
return count + ' 条规则';
|
||||
}
|
||||
},
|
||||
];
|
||||
//查询数据
|
||||
@@ -50,8 +49,8 @@ export const searchFormSchema: FormSchema[] = [
|
||||
component: 'JInput',
|
||||
},
|
||||
{
|
||||
label: "创建人",
|
||||
field: "createBy",
|
||||
label: "接口地址",
|
||||
field: "requestUrl",
|
||||
component: 'JInput',
|
||||
},
|
||||
];
|
||||
@@ -68,12 +67,35 @@ export const formSchema: FormSchema[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '原始地址',
|
||||
label: '原始接口',
|
||||
field: 'originUrl',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '当前系统的原始接口地址,如 /sys/user/list',
|
||||
},
|
||||
helpMessage: '当前系统中被代理的原始接口路径',
|
||||
dynamicRules: () => {
|
||||
return [
|
||||
{ required: true, message: '请输入原始接口路径!' },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value && !value.startsWith('/')) {
|
||||
return Promise.reject('原始接口路径必须以 / 开头');
|
||||
}
|
||||
if (value && value.includes('//')) {
|
||||
return Promise.reject('原始接口路径不能包含 //');
|
||||
}
|
||||
if (value && value.includes('..')) {
|
||||
return Promise.reject('原始接口路径不能包含 ..');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '请求方法',
|
||||
label: '请求方式',
|
||||
field: 'requestMethod',
|
||||
component: 'JSearchSelect',
|
||||
componentProps:{
|
||||
@@ -112,7 +134,7 @@ export const formSchema: FormSchema[] = [
|
||||
},
|
||||
dynamicRules: ({model,schema}) => {
|
||||
return [
|
||||
{ required: true, message: '请输入请求方法!'},
|
||||
{ required: true, message: '请输入请求方式!'},
|
||||
];
|
||||
},
|
||||
},
|
||||
@@ -123,14 +145,36 @@ export const formSchema: FormSchema[] = [
|
||||
dynamicDisabled:true
|
||||
},
|
||||
{
|
||||
label: 'IP 黑名单',
|
||||
field: 'blackList',
|
||||
component: 'Input',
|
||||
label: 'IP 白名单',
|
||||
field: 'whiteList',
|
||||
helpMessage: '支持精确IP、CIDR网段(如192.168.1.0/24)、通配符(如10.2.3.*),每行一个或逗号分隔,为空则不限制',
|
||||
component: 'InputTextArea',
|
||||
slot: 'whiteListSlot',
|
||||
componentProps: {
|
||||
rows: 5,
|
||||
placeholder: '示例:\n192.168.1.100\n10.0.0.0/8\n172.16.*.*',
|
||||
},
|
||||
colProps: { span: 24 },
|
||||
},
|
||||
{
|
||||
label: '请求体内容',
|
||||
component:"Input",
|
||||
field: 'body'
|
||||
label: '备注',
|
||||
field: 'comment',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
rows: 2,
|
||||
placeholder: '请输入白名单备注说明',
|
||||
},
|
||||
colProps: { span: 24 },
|
||||
},
|
||||
{
|
||||
label: '接口描述',
|
||||
field: 'description',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
rows: 3,
|
||||
placeholder: '请输入接口描述',
|
||||
},
|
||||
colProps: { span: 24 },
|
||||
},
|
||||
{
|
||||
label: '删除标识',
|
||||
@@ -240,6 +284,21 @@ export const openApiHeaderJVxeColumns: JVxeColumn[] = [
|
||||
defaultValue:'',
|
||||
customValue: ['1','0']
|
||||
},
|
||||
{
|
||||
title: '参数类型',
|
||||
key: 'paramType',
|
||||
type: JVxeTypes.select,
|
||||
width: '120px',
|
||||
options: [
|
||||
{ title: 'string', value: 'string' },
|
||||
{ title: 'integer', value: 'integer' },
|
||||
{ title: 'number', value: 'number' },
|
||||
{ title: 'boolean', value: 'boolean' },
|
||||
{ title: 'array', value: 'array' },
|
||||
{ title: 'object', value: 'object' },
|
||||
],
|
||||
defaultValue: 'string',
|
||||
},
|
||||
{
|
||||
title: '默认值',
|
||||
key: 'defaultValue',
|
||||
@@ -248,6 +307,14 @@ export const openApiHeaderJVxeColumns: JVxeColumn[] = [
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue:'',
|
||||
},
|
||||
{
|
||||
title: '示例值',
|
||||
key: 'example',
|
||||
type: JVxeTypes.input,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
key: 'note',
|
||||
@@ -284,6 +351,21 @@ export const openApiParamJVxeColumns: JVxeColumn[] = [
|
||||
defaultValue:'',
|
||||
customValue: ['1','0']
|
||||
},
|
||||
{
|
||||
title: '参数类型',
|
||||
key: 'paramType',
|
||||
type: JVxeTypes.select,
|
||||
width: '120px',
|
||||
options: [
|
||||
{ title: 'string', value: 'string' },
|
||||
{ title: 'integer', value: 'integer' },
|
||||
{ title: 'number', value: 'number' },
|
||||
{ title: 'boolean', value: 'boolean' },
|
||||
{ title: 'array', value: 'array' },
|
||||
{ title: 'object', value: 'object' },
|
||||
],
|
||||
defaultValue: 'string',
|
||||
},
|
||||
{
|
||||
title: '默认值',
|
||||
key: 'defaultValue',
|
||||
@@ -292,6 +374,14 @@ export const openApiParamJVxeColumns: JVxeColumn[] = [
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue:'',
|
||||
},
|
||||
{
|
||||
title: '示例值',
|
||||
key: 'example',
|
||||
type: JVxeTypes.input,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
key: 'note',
|
||||
@@ -301,12 +391,45 @@ export const openApiParamJVxeColumns: JVxeColumn[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export const responseFieldJVxeColumns: JVxeColumn[] = [
|
||||
{
|
||||
title: '字段名',
|
||||
key: 'fieldName',
|
||||
type: JVxeTypes.input,
|
||||
width: '200px',
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: 'fieldType',
|
||||
type: JVxeTypes.select,
|
||||
width: '120px',
|
||||
options: [
|
||||
{ title: 'string', value: 'string' },
|
||||
{ title: 'integer', value: 'integer' },
|
||||
{ title: 'number', value: 'number' },
|
||||
{ title: 'boolean', value: 'boolean' },
|
||||
{ title: 'array', value: 'array' },
|
||||
{ title: 'object', value: 'object' },
|
||||
],
|
||||
defaultValue: 'string',
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
key: 'fieldDesc',
|
||||
type: JVxeTypes.input,
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue: '',
|
||||
},
|
||||
];
|
||||
|
||||
// 高级查询数据
|
||||
export const superQuerySchema = {
|
||||
name: {title: '接口名称',order: 0,view: 'text', type: 'string',},
|
||||
requestMethod: {title: '请求方法',order: 1,view: 'list', type: 'string',dictCode: '',},
|
||||
requestMethod: {title: '请求方式',order: 1,view: 'list', type: 'string',dictCode: '',},
|
||||
requestUrl: {title: '接口地址',order: 2,view: 'text', type: 'string',},
|
||||
blackList: {title: 'IP 黑名单',order: 3,view: 'text', type: 'string',},
|
||||
whiteList: {title: 'IP 白名单',order: 3,view: 'text', type: 'string',},
|
||||
status: {title: '状态',order: 5,view: 'number', type: 'number',},
|
||||
createBy: {title: '创建人',order: 6,view: 'text', type: 'string',},
|
||||
createTime: {title: '创建时间',order: 7,view: 'datetime', type: 'string',},
|
||||
|
||||
@@ -1,48 +1,84 @@
|
||||
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';
|
||||
import { BasicColumn } from '/@/components/Table';
|
||||
import { FormSchema } from '/@/components/Table';
|
||||
|
||||
//列表数据
|
||||
export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: '授权名称',
|
||||
align: "center",
|
||||
dataIndex: 'name'
|
||||
title: '授权对象',
|
||||
align: 'center',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: 'AK',
|
||||
align: "center",
|
||||
dataIndex: 'ak'
|
||||
},
|
||||
{
|
||||
title: 'SK',
|
||||
align: "center",
|
||||
dataIndex: 'sk'
|
||||
title: '访问密钥(AK)',
|
||||
align: 'center',
|
||||
dataIndex: 'ak',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
align: "center",
|
||||
dataIndex: 'createBy'
|
||||
align: 'center',
|
||||
dataIndex: 'createBy',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
align: "center",
|
||||
dataIndex: 'createTime'
|
||||
align: 'center',
|
||||
dataIndex: 'createTime',
|
||||
},
|
||||
];
|
||||
|
||||
//查询数据
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{
|
||||
label: '授权对象',
|
||||
field: 'name',
|
||||
component: 'JInput',
|
||||
},
|
||||
{
|
||||
label: '访问密钥',
|
||||
field: 'ak',
|
||||
component: 'JInput',
|
||||
},
|
||||
];
|
||||
|
||||
//授权表单数据
|
||||
export const authFormSchema: FormSchema[] = [
|
||||
{
|
||||
label: '授权对象',
|
||||
field: 'name',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
field: 'ak',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
field: 'sk',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
field: 'systemUserId',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
// {
|
||||
// title: '关联系统用户名',
|
||||
// align: "center",
|
||||
// dataIndex: 'createBy',
|
||||
// },
|
||||
];
|
||||
|
||||
// 高级查询数据
|
||||
export const superQuerySchema = {
|
||||
name: {title: '授权名称',order: 0,view: 'text', type: 'string',},
|
||||
ak: {title: 'AK',order: 1,view: 'text', type: 'string',},
|
||||
sk: {title: 'SK',order: 2,view: 'text', type: 'string',},
|
||||
createBy: {title: '关联系统用户名',order: 3,view: 'text', type: 'string',},
|
||||
createTime: {title: '创建时间',order: 4,view: 'datetime', type: 'string',},
|
||||
// systemUserId: {title: '关联系统用户名',order: 5,view: 'text', type: 'string',},
|
||||
name: { title: '授权对象', order: 0, view: 'text', type: 'string' },
|
||||
ak: { title: '访问密钥(AK)', order: 1, view: 'text', type: 'string' },
|
||||
sk: { title: '签名密钥(SK)', order: 2, view: 'text', type: 'string' },
|
||||
createBy: { title: '创建人', order: 3, view: 'text', type: 'string' },
|
||||
createTime: { title: '创建时间', order: 4, view: 'datetime', type: 'string' },
|
||||
};
|
||||
|
||||
@@ -1,44 +1,12 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<!--查询区域-->
|
||||
<div class="jeecg-basic-table-form-container">
|
||||
<a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
|
||||
<a-row :gutter="24">
|
||||
<a-col :lg="6">
|
||||
<a-form-item name="name">
|
||||
<template #label><span title="授权名称">授权名称</span></template>
|
||||
<a-input placeholder="请输入授权名称" v-model:value="queryParam.name" allow-clear ></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6">
|
||||
<a-form-item name="createBy">
|
||||
<template #label><span title="关联系统用户名">关联系统用户名</span></template>
|
||||
<JSearchSelect dict="sys_user,username,username" v-model:value="queryParam.createBy" placeholder="请输入关联系统用户名" allow-clear ></JSearchSelect>
|
||||
<!-- <a-input placeholder="请输入关联系统用户名" v-model:value="queryParam.systemUserId" allow-clear ></a-input>-->
|
||||
</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 @click="toggleSearchStatus = !toggleSearchStatus" style="margin-left: 8px">
|
||||
{{ toggleSearchStatus ? '收起' : '展开' }}
|
||||
<Icon :icon="toggleSearchStatus ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" />
|
||||
</a>
|
||||
</a-col>
|
||||
</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
<div>
|
||||
<!--引用表格-->
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<!--插槽:table标题-->
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" v-auth="'openapi:open_api_auth:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
|
||||
<a-button type="primary" v-auth="'openapi:open_api_auth:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
|
||||
<j-upload-button type="primary" v-auth="'openapi:open_api_auth:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
|
||||
<a-button type="primary" v-auth="'openapi:open_api_auth:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
|
||||
<a-button type="primary" v-auth="'openapi:open_api_auth:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
|
||||
<j-upload-button type="primary" v-auth="'openapi:open_api_auth:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
|
||||
<a-dropdown v-if="selectedRowKeys.length > 0">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
@@ -57,15 +25,15 @@
|
||||
</template>
|
||||
<!--操作栏-->
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
|
||||
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
|
||||
</template>
|
||||
<!--字段回显插槽-->
|
||||
<template v-slot:bodyCell="{ column, record, index, text }">
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<OpenApiAuthModal ref="registerModal" @success="handleSuccess"></OpenApiAuthModal>
|
||||
<AuthModal ref="authModal" @success="handleSuccess"></AuthModal>
|
||||
<OpenApiAuthDrawer @register="registerAuthDrawer" @success="handleSuccess" />
|
||||
<AuthDrawer @register="registerPermDrawer" @success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -73,63 +41,51 @@
|
||||
import { ref, reactive } from 'vue';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { columns, superQuerySchema } from './OpenApiAuth.data';
|
||||
import {
|
||||
list,
|
||||
deleteOne,
|
||||
batchDelete,
|
||||
getImportUrl,
|
||||
getExportUrl,
|
||||
getGenAKSK, saveOrUpdate
|
||||
} from "./OpenApiAuth.api";
|
||||
import OpenApiAuthModal from './components/OpenApiAuthModal.vue'
|
||||
import AuthModal from './components/AuthModal.vue'
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import JSearchSelect from "../../components/Form/src/jeecg/components/JSearchSelect.vue";
|
||||
import { useDrawer } from '/@/components/Drawer';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { columns, searchFormSchema, superQuerySchema } from './OpenApiAuth.data';
|
||||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl, getGenAKSK, saveOrUpdate } from './OpenApiAuth.api';
|
||||
import OpenApiAuthDrawer from './components/OpenApiAuthDrawer.vue';
|
||||
import AuthDrawer from './components/AuthDrawer.vue';
|
||||
|
||||
const formRef = ref();
|
||||
const queryParam = reactive<any>({});
|
||||
const toggleSearchStatus = ref<boolean>(false);
|
||||
const registerModal = ref();
|
||||
const authModal = ref();
|
||||
const userStore = useUserStore();
|
||||
const { createMessage } = useMessage();
|
||||
const [registerAuthDrawer, { openDrawer: openAuthDrawer }] = useDrawer();
|
||||
const [registerPermDrawer, { openDrawer: openPermDrawer }] = useDrawer();
|
||||
|
||||
//注册table数据
|
||||
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
|
||||
tableProps: {
|
||||
title: '授权管理',
|
||||
api: list,
|
||||
columns,
|
||||
canResize:false,
|
||||
useSearchForm: false,
|
||||
canResize: false,
|
||||
formConfig: {
|
||||
schemas: searchFormSchema,
|
||||
autoSubmitOnEnter: true,
|
||||
showAdvancedButton: true,
|
||||
fieldMapToNumber: [],
|
||||
fieldMapToTime: [],
|
||||
},
|
||||
actionColumn: {
|
||||
width: 200,
|
||||
width: 220,
|
||||
fixed: 'right',
|
||||
},
|
||||
beforeFetch: async (params) => {
|
||||
beforeFetch: (params) => {
|
||||
return Object.assign(params, queryParam);
|
||||
},
|
||||
},
|
||||
exportConfig: {
|
||||
name: "授权管理",
|
||||
name: '授权管理',
|
||||
url: getExportUrl,
|
||||
params: queryParam,
|
||||
},
|
||||
importConfig: {
|
||||
url: getImportUrl,
|
||||
success: handleSuccess
|
||||
},
|
||||
});
|
||||
const [registerTable, { reload, updateTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||
const labelCol = reactive({
|
||||
xs:24,
|
||||
sm:10,
|
||||
xl:6,
|
||||
xxl:10
|
||||
});
|
||||
const wrapperCol = reactive({
|
||||
xs: 24,
|
||||
sm: 20,
|
||||
importConfig: {
|
||||
url: getImportUrl,
|
||||
success: handleSuccess,
|
||||
},
|
||||
});
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||
|
||||
// 高级查询配置
|
||||
const superQueryConfig = reactive(superQuerySchema);
|
||||
@@ -141,163 +97,144 @@
|
||||
Object.keys(params).map((k) => {
|
||||
queryParam[k] = params[k];
|
||||
});
|
||||
searchQuery();
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增事件
|
||||
*/
|
||||
function handleAdd() {
|
||||
registerModal.value.disableSubmit = false;
|
||||
registerModal.value.add();
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑事件
|
||||
*/
|
||||
function handleAuth(record: Recordable) {
|
||||
authModal.value.disableSubmit = false;
|
||||
authModal.value.edit(record);
|
||||
openAuthDrawer(true, {
|
||||
isUpdate: false,
|
||||
showFooter: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑事件
|
||||
*/
|
||||
function handleEdit(record: Recordable) {
|
||||
registerModal.value.disableSubmit = false;
|
||||
registerModal.value.authDrawerOpen = true;
|
||||
registerModal.value.edit(record);
|
||||
openAuthDrawer(true, {
|
||||
record,
|
||||
isUpdate: true,
|
||||
showFooter: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置事件
|
||||
* @param record
|
||||
* 授权事件
|
||||
*/
|
||||
function handleAuth(record: Recordable) {
|
||||
openPermDrawer(true, { record });
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置AK/SK
|
||||
*/
|
||||
async function handleReset(record: Recordable) {
|
||||
const AKSKObj = await getGenAKSK({});
|
||||
record.ak = AKSKObj[0];
|
||||
record.sk = AKSKObj[1];
|
||||
saveOrUpdate(record,true);
|
||||
// handleSuccess;
|
||||
|
||||
await saveOrUpdate(record, true);
|
||||
reload();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 详情
|
||||
*/
|
||||
function handleDetail(record: Recordable) {
|
||||
registerModal.value.disableSubmit = true;
|
||||
registerModal.value.edit(record);
|
||||
openAuthDrawer(true, {
|
||||
record,
|
||||
isUpdate: true,
|
||||
showFooter: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除事件
|
||||
*/
|
||||
async function handleDelete(record) {
|
||||
await deleteOne({ id: record.id }, handleSuccess);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 批量删除事件
|
||||
*/
|
||||
async function batchHandleDelete() {
|
||||
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 成功回调
|
||||
*/
|
||||
function handleSuccess() {
|
||||
(selectedRowKeys.value = []) && reload();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 操作栏
|
||||
*/
|
||||
/**
|
||||
* 复制密钥
|
||||
*/
|
||||
async function handleCopyKeys(record: Recordable) {
|
||||
const text = `访问密钥(AK): ${record.ak}\n签名密钥(SK): ${record.sk}`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
createMessage.success('密钥已复制到剪贴板');
|
||||
} catch (_e) {
|
||||
createMessage.error('复制失败,请手动复制');
|
||||
}
|
||||
}
|
||||
|
||||
function getTableAction(record) {
|
||||
return [
|
||||
{
|
||||
label: '授权',
|
||||
onClick: handleAuth.bind(null, record),
|
||||
auth: 'openapi:open_api_auth:edit'
|
||||
label: '复制密钥',
|
||||
onClick: handleCopyKeys.bind(null, record),
|
||||
},
|
||||
{
|
||||
label: '重置',
|
||||
popConfirm: {
|
||||
title: '是否重置AK,SK',
|
||||
confirm: handleReset.bind(null, record),
|
||||
placement: 'topLeft',
|
||||
},
|
||||
auth: 'openapi:open_api_auth:edit'
|
||||
label: '分配接口',
|
||||
onClick: handleAuth.bind(null, record),
|
||||
auth: 'openapi:open_api_auth:edit',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 下拉操作栏
|
||||
*/
|
||||
function getDropDownAction(record) {
|
||||
return [
|
||||
{
|
||||
label: '详情',
|
||||
onClick: handleDetail.bind(null, record),
|
||||
}, {
|
||||
label: '修改对象',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
auth: 'openapi:open_api_auth:edit',
|
||||
},
|
||||
{
|
||||
label: '重置密钥',
|
||||
popConfirm: {
|
||||
title: '原密钥将失效,确认重置?',
|
||||
confirm: handleReset.bind(null, record),
|
||||
placement: 'topLeft',
|
||||
},
|
||||
auth: 'openapi:open_api_auth:edit',
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
popConfirm: {
|
||||
title: '是否确认删除',
|
||||
confirm: handleDelete.bind(null, record),
|
||||
placement: 'topLeft',
|
||||
},
|
||||
auth: 'openapi:open_api_auth:delete'
|
||||
}
|
||||
]
|
||||
auth: 'openapi:open_api_auth:delete',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
function searchQuery() {
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
function searchReset() {
|
||||
formRef.value.resetFields();
|
||||
selectedRowKeys.value = [];
|
||||
//刷新数据
|
||||
reload();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.jeecg-basic-table-form-container {
|
||||
padding: 0;
|
||||
.table-page-search-submitButtons {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.query-group-cust{
|
||||
min-width: 100px !important;
|
||||
}
|
||||
.query-group-split-cust{
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
text-align: center
|
||||
}
|
||||
.ant-form-item:not(.ant-form-item-with-help){
|
||||
margin-bottom: 16px;
|
||||
height: 32px;
|
||||
}
|
||||
:deep(.ant-picker),:deep(.ant-input-number){
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.ant-picker),:deep(.ant-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -41,31 +41,30 @@
|
||||
</template>
|
||||
<!--字段回显插槽-->
|
||||
<template v-slot:bodyCell="{ column, record, index, text }">
|
||||
<template v-if="column.dataIndex === 'requestUrl'">
|
||||
<a @click="handleCopyUrl(record)" title="点击复制完整接口地址">{{ text }}</a>
|
||||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<!-- 表单区域 -->
|
||||
<OpenApiModal @register="registerModal" @success="handleSuccess"></OpenApiModal>
|
||||
<OpenApiDrawer @register="registerDrawer" @success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="openapi-openApi" setup>
|
||||
import {ref, reactive, computed, unref} from 'vue';
|
||||
import {BasicTable, useTable, TableAction} from '/@/components/Table';
|
||||
import { useListPage } from '/@/hooks/system/useListPage'
|
||||
import {useModal} from '/@/components/Modal';
|
||||
import OpenApiModal from './components/OpenApiModal.vue'
|
||||
import OpenApiHeaderSubTable from './subTables/OpenApiHeaderSubTable.vue'
|
||||
import OpenApiParamSubTable from './subTables/OpenApiParamSubTable.vue'
|
||||
import {columns, searchFormSchema, superQuerySchema} from './OpenApi.data';
|
||||
import {list, deleteOne, batchDelete, getImportUrl,getExportUrl} from './OpenApi.api';
|
||||
import {downloadFile} from '/@/utils/common/renderUtils';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { ref, reactive } from 'vue';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { useDrawer } from '/@/components/Drawer';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import OpenApiDrawer from './components/OpenApiDrawer.vue';
|
||||
import { columns, searchFormSchema, superQuerySchema } from './OpenApi.data';
|
||||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './OpenApi.api';
|
||||
const queryParam = reactive<any>({});
|
||||
// 展开key
|
||||
const expandedRowKeys = ref<any[]>([]);
|
||||
//注册model
|
||||
const [registerModal, {openModal}] = useModal();
|
||||
const userStore = useUserStore();
|
||||
const { createMessage } = useMessage();
|
||||
const API_DOMAIN = import.meta.env.VITE_GLOB_DOMAIN_URL;
|
||||
const [registerDrawer, { openDrawer }] = useDrawer();
|
||||
//注册table数据
|
||||
const { prefixCls,tableContext,onExportXls,onImportXls } = useListPage({
|
||||
tableProps:{
|
||||
@@ -84,7 +83,7 @@
|
||||
],
|
||||
},
|
||||
actionColumn: {
|
||||
width: 120,
|
||||
width: 200,
|
||||
fixed:'right'
|
||||
},
|
||||
beforeFetch: (params) => {
|
||||
@@ -130,7 +129,7 @@
|
||||
* 新增事件
|
||||
*/
|
||||
function handleAdd() {
|
||||
openModal(true, {
|
||||
openDrawer(true, {
|
||||
isUpdate: false,
|
||||
showFooter: true,
|
||||
});
|
||||
@@ -139,7 +138,7 @@
|
||||
* 编辑事件
|
||||
*/
|
||||
function handleEdit(record: Recordable) {
|
||||
openModal(true, {
|
||||
openDrawer(true, {
|
||||
record,
|
||||
isUpdate: true,
|
||||
showFooter: true,
|
||||
@@ -149,7 +148,7 @@
|
||||
* 详情
|
||||
*/
|
||||
function handleDetail(record: Recordable) {
|
||||
openModal(true, {
|
||||
openDrawer(true, {
|
||||
record,
|
||||
isUpdate: true,
|
||||
showFooter: false,
|
||||
@@ -176,13 +175,26 @@
|
||||
/**
|
||||
* 操作栏
|
||||
*/
|
||||
/**
|
||||
* 复制接口地址
|
||||
*/
|
||||
async function handleCopyUrl(record: Recordable) {
|
||||
const url = API_DOMAIN + '/openapi/call/' + record.requestUrl;
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
createMessage.success('接口地址已复制');
|
||||
} catch (_e) {
|
||||
createMessage.error('复制失败,请手动复制');
|
||||
}
|
||||
}
|
||||
|
||||
function getTableAction(record){
|
||||
return [
|
||||
{
|
||||
label: '编辑',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
auth: 'openapi:open_api:edit'
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
185
jeecgboot-vue3/src/views/openapi/components/AuthDrawer.vue
Normal file
185
jeecgboot-vue3/src/views/openapi/components/AuthDrawer.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<BasicDrawer
|
||||
v-bind="$attrs"
|
||||
@register="registerDrawer"
|
||||
title="接口授权"
|
||||
width="720px"
|
||||
destroyOnClose
|
||||
@ok="handleSubmit"
|
||||
>
|
||||
<a-spin :spinning="confirmLoading">
|
||||
<a-row :gutter="[12, 12]">
|
||||
<a-col :span="12" v-for="item in apiList" :key="item.id">
|
||||
<a-card
|
||||
:class="['auth-api-card', { 'auth-api-card--checked': item.checked }]"
|
||||
hoverable
|
||||
:body-style="{ padding: '12px' }"
|
||||
@click="handleSelect(item)"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span class="auth-api-name">{{ item.name }}</span>
|
||||
<a-checkbox v-model:checked="item.checked" @click.stop @change="(e) => handleChange(e, item)" />
|
||||
</div>
|
||||
<div style="margin-top: 6px; color: #888; font-size: 12px">
|
||||
<a-tag :color="getMethodColor(item.requestMethod)">{{ item.requestMethod }}</a-tag>
|
||||
<span style="margin-left: 4px">{{ item.requestUrl }}</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div v-if="apiList.length === 0 && !confirmLoading" style="text-align: center; padding: 40px 0; color: #999">
|
||||
暂无接口数据
|
||||
</div>
|
||||
<div v-if="total > 0" style="margin-top: 16px; text-align: right">
|
||||
<a-pagination
|
||||
:current="pageNo"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="['10', '20', '30']"
|
||||
:total="total"
|
||||
show-quick-jumper
|
||||
show-size-changer
|
||||
size="small"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
|
||||
import { getApiList, getPermissionList, permissionAddFunction } from '../OpenApiAuth.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
const { createMessage } = useMessage();
|
||||
const confirmLoading = ref(false);
|
||||
const apiAuthId = ref('');
|
||||
const apiList = ref<any[]>([]);
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const pageNo = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
|
||||
selectedRowKeys.value = [];
|
||||
apiList.value = [];
|
||||
pageNo.value = 1;
|
||||
pageSize.value = 10;
|
||||
total.value = 0;
|
||||
apiAuthId.value = data.record?.id || '';
|
||||
|
||||
// Load existing permissions
|
||||
try {
|
||||
const permRes = await getPermissionList({ apiAuthId: apiAuthId.value });
|
||||
if (permRes && permRes.length > 0) {
|
||||
permRes.forEach((item) => {
|
||||
if (item.ifCheckBox == '1') {
|
||||
selectedRowKeys.value.push(item.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (_e) {
|
||||
// ignore
|
||||
}
|
||||
await reload();
|
||||
});
|
||||
|
||||
async function reload() {
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
const res = await getApiList({
|
||||
pageNo: pageNo.value,
|
||||
pageSize: pageSize.value,
|
||||
column: 'createTime',
|
||||
order: 'desc',
|
||||
});
|
||||
if (res.success) {
|
||||
const records = res.result.records || [];
|
||||
records.forEach((item) => {
|
||||
item.checked = selectedRowKeys.value.includes(item.id);
|
||||
});
|
||||
apiList.value = records;
|
||||
total.value = res.result.total || 0;
|
||||
} else {
|
||||
apiList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(item) {
|
||||
item.checked = !item.checked;
|
||||
toggleSelection(item.id, item.checked);
|
||||
}
|
||||
|
||||
function handleChange(e, item) {
|
||||
toggleSelection(item.id, e.target.checked);
|
||||
}
|
||||
|
||||
function toggleSelection(id: string, checked: boolean) {
|
||||
const idx = selectedRowKeys.value.indexOf(id);
|
||||
if (checked && idx === -1) {
|
||||
selectedRowKeys.value.push(id);
|
||||
} else if (!checked && idx !== -1) {
|
||||
selectedRowKeys.value.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(page, current) {
|
||||
pageNo.value = page;
|
||||
pageSize.value = current;
|
||||
reload();
|
||||
}
|
||||
|
||||
function getMethodColor(method: string) {
|
||||
const map = { GET: 'green', POST: 'blue', PUT: 'orange', DELETE: 'red', PATCH: 'purple' };
|
||||
return map[method] || 'default';
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
setDrawerProps({ confirmLoading: true });
|
||||
const res = await permissionAddFunction({
|
||||
apiId: selectedRowKeys.value.join(','),
|
||||
apiAuthId: apiAuthId.value,
|
||||
});
|
||||
if (res.success) {
|
||||
createMessage.success(res.message);
|
||||
closeDrawer();
|
||||
emit('success');
|
||||
} else {
|
||||
createMessage.warning(res.message);
|
||||
}
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
setDrawerProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.auth-api-card {
|
||||
transition: all 0.2s;
|
||||
border: 1px solid #d9d9d9;
|
||||
|
||||
&--checked {
|
||||
border-color: #1890ff;
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-api-name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 200px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<BasicDrawer
|
||||
v-bind="$attrs"
|
||||
@register="registerDrawer"
|
||||
:title="title"
|
||||
width="600px"
|
||||
destroyOnClose
|
||||
@ok="handleSubmit"
|
||||
:showFooter="showFooter"
|
||||
>
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { authFormSchema } from '../OpenApiAuth.data';
|
||||
import { saveOrUpdate } from '../OpenApiAuth.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { USER_INFO_KEY } from '/@/enums/cacheEnum';
|
||||
import { getAuthCache } from '/@/utils/auth';
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
const { createMessage } = useMessage();
|
||||
const isUpdate = ref(false);
|
||||
const formDisabled = ref(false);
|
||||
const showFooter = ref(true);
|
||||
|
||||
const [registerForm, { resetFields, setFieldsValue, validate, setProps }] = useForm({
|
||||
labelWidth: 100,
|
||||
schemas: authFormSchema,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 24 },
|
||||
});
|
||||
|
||||
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
|
||||
await resetFields();
|
||||
showFooter.value = !!data?.showFooter;
|
||||
setDrawerProps({ confirmLoading: false, showFooter: showFooter.value });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
formDisabled.value = !data?.showFooter;
|
||||
|
||||
if (unref(isUpdate)) {
|
||||
await setFieldsValue({ ...data.record });
|
||||
} else {
|
||||
// New record: set current user
|
||||
const userData = getAuthCache(USER_INFO_KEY) as any;
|
||||
await setFieldsValue({
|
||||
systemUserId: userData?.id || '',
|
||||
});
|
||||
}
|
||||
setProps({ disabled: !data?.showFooter });
|
||||
});
|
||||
|
||||
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(formDisabled) ? '编辑' : '详情'));
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
setDrawerProps({ confirmLoading: true });
|
||||
const res = await saveOrUpdate(values, isUpdate.value);
|
||||
if (res.success) {
|
||||
createMessage.success(res.message);
|
||||
closeDrawer();
|
||||
emit('success');
|
||||
} else {
|
||||
createMessage.warning(res.message);
|
||||
}
|
||||
} finally {
|
||||
setDrawerProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -5,18 +5,18 @@
|
||||
<a-form ref="formRef" class="antd-modal-form" :labelCol="labelCol" :wrapperCol="wrapperCol" name="OpenApiAuthForm">
|
||||
<a-row>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="授权名称" v-bind="validateInfos.name" id="OpenApiAuthForm-name" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入授权名称" allow-clear ></a-input>
|
||||
<a-form-item label="授权对象" v-bind="validateInfos.name" id="OpenApiAuthForm-name" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入授权对象" allow-clear ></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="AK" v-bind="validateInfos.ak" id="OpenApiAuthForm-ak" name="ak">
|
||||
<a-input v-model:value="formData.ak" placeholder="请输入AK" disabled allow-clear ></a-input>
|
||||
<a-form-item label="访问密钥(AK)" v-bind="validateInfos.ak" id="OpenApiAuthForm-ak" name="ak">
|
||||
<a-input v-model:value="formData.ak" placeholder="自动生成" disabled allow-clear ></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="SK" v-bind="validateInfos.sk" id="OpenApiAuthForm-sk" name="sk">
|
||||
<a-input v-model:value="formData.sk" placeholder="请输入SK" disabled allow-clear ></a-input>
|
||||
<a-form-item label="签名密钥(SK)" v-bind="validateInfos.sk" id="OpenApiAuthForm-sk" name="sk">
|
||||
<a-input v-model:value="formData.sk" placeholder="自动生成" disabled allow-clear ></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- <a-col :span="24">-->
|
||||
@@ -63,7 +63,7 @@
|
||||
const confirmLoading = ref<boolean>(false);
|
||||
//表单验证
|
||||
const validatorRules = reactive({
|
||||
name:[{ required: true, message: '请输入授权名称!'},],
|
||||
name:[{ required: true, message: '请输入授权对象!'},],
|
||||
systemUserId:[{ required: true, message: '请输入关联系统用户名!'},],
|
||||
});
|
||||
const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: false });
|
||||
|
||||
269
jeecgboot-vue3/src/views/openapi/components/OpenApiDrawer.vue
Normal file
269
jeecgboot-vue3/src/views/openapi/components/OpenApiDrawer.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<BasicDrawer
|
||||
v-bind="$attrs"
|
||||
@register="registerDrawer"
|
||||
:title="title"
|
||||
width="90%"
|
||||
destroyOnClose
|
||||
@ok="handleSubmit"
|
||||
:showFooter="showFooter"
|
||||
>
|
||||
<!-- 上部:基本信息表单 -->
|
||||
<BasicForm @register="registerForm" ref="formRef">
|
||||
<template #whiteListSlot="{ model, field }">
|
||||
<a-textarea
|
||||
v-model:value="model[field]"
|
||||
:rows="5"
|
||||
placeholder="示例: 192.168.1.100 10.0.0.0/8 172.16.*.*"
|
||||
:disabled="formDisabled"
|
||||
/>
|
||||
<!-- 标签预览 -->
|
||||
<div v-if="model[field]" style="margin-top: 8px">
|
||||
<a-tag
|
||||
v-for="item in parseWhiteList(model[field])"
|
||||
:key="item"
|
||||
color="green"
|
||||
style="margin-bottom: 4px"
|
||||
>
|
||||
{{ item }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<!-- 整理按钮 -->
|
||||
<div v-if="model[field] && !formDisabled" style="margin-top: 4px; text-align: right">
|
||||
<a-button size="small" @click="formatWhiteList(model, field)">整 理</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</BasicForm>
|
||||
|
||||
<!-- 下部:Tabs -->
|
||||
<a-tabs v-model:activeKey="activeTab" style="margin-top: 16px">
|
||||
<a-tab-pane key="headers" tab="请求头">
|
||||
<JVxeTable
|
||||
keep-source
|
||||
ref="openApiHeader"
|
||||
:loading="openApiHeaderTable.loading"
|
||||
:columns="openApiHeaderTable.columns"
|
||||
:dataSource="openApiHeaderTable.dataSource"
|
||||
:height="240"
|
||||
:disabled="formDisabled"
|
||||
:rowNumber="true"
|
||||
:rowSelection="true"
|
||||
:toolbar="true"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="params" tab="请求参数">
|
||||
<JVxeTable
|
||||
keep-source
|
||||
ref="openApiParam"
|
||||
:loading="openApiParamTable.loading"
|
||||
:columns="openApiParamTable.columns"
|
||||
:dataSource="openApiParamTable.dataSource"
|
||||
:height="240"
|
||||
:disabled="formDisabled"
|
||||
:rowNumber="true"
|
||||
:rowSelection="true"
|
||||
:toolbar="true"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="body" tab="请求体">
|
||||
<div style="border: 1px solid #d9d9d9; border-radius: 4px; min-height: 300px">
|
||||
<CodeEditor v-model:value="bodyContent" mode="application/json" :readonly="formDisabled" />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="response" tab="响应配置">
|
||||
<div style="margin-bottom: 16px">
|
||||
<h4 style="margin-bottom: 8px">响应示例</h4>
|
||||
<div style="border: 1px solid #d9d9d9; border-radius: 4px; min-height: 200px">
|
||||
<CodeEditor v-model:value="responseExample" mode="application/json" :readonly="formDisabled" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 style="margin-bottom: 8px">响应字段说明</h4>
|
||||
<JVxeTable
|
||||
keep-source
|
||||
ref="responseField"
|
||||
:loading="responseFieldTable.loading"
|
||||
:columns="responseFieldTable.columns"
|
||||
:dataSource="responseFieldTable.dataSource"
|
||||
:height="240"
|
||||
:disabled="formDisabled"
|
||||
:rowNumber="true"
|
||||
:rowSelection="true"
|
||||
:toolbar="true"
|
||||
/>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, unref, reactive } from 'vue';
|
||||
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { CodeEditor } from '/@/components/CodeEditor';
|
||||
import {
|
||||
formSchema,
|
||||
openApiHeaderJVxeColumns,
|
||||
openApiParamJVxeColumns,
|
||||
responseFieldJVxeColumns,
|
||||
} from '../OpenApi.data';
|
||||
import { saveOrUpdate, getGenPath } from '../OpenApi.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
const $message = useMessage();
|
||||
const isUpdate = ref(true);
|
||||
const formDisabled = ref(false);
|
||||
const showFooter = ref(true);
|
||||
const activeTab = ref('headers');
|
||||
const bodyContent = ref('');
|
||||
const responseExample = ref('');
|
||||
|
||||
const openApiHeader = ref();
|
||||
const openApiParam = ref();
|
||||
const responseField = ref();
|
||||
|
||||
const openApiHeaderTable = reactive({
|
||||
loading: false,
|
||||
dataSource: [] as any[],
|
||||
columns: openApiHeaderJVxeColumns,
|
||||
});
|
||||
const openApiParamTable = reactive({
|
||||
loading: false,
|
||||
dataSource: [] as any[],
|
||||
columns: openApiParamJVxeColumns,
|
||||
});
|
||||
const responseFieldTable = reactive({
|
||||
loading: false,
|
||||
dataSource: [] as any[],
|
||||
columns: responseFieldJVxeColumns,
|
||||
});
|
||||
|
||||
const [registerForm, { setProps, resetFields, setFieldsValue, validate }] = useForm({
|
||||
labelWidth: 100,
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 12 },
|
||||
});
|
||||
|
||||
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
|
||||
await reset();
|
||||
showFooter.value = !!data?.showFooter;
|
||||
setDrawerProps({ confirmLoading: false, showFooter: showFooter.value });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
formDisabled.value = !data?.showFooter;
|
||||
|
||||
if (unref(isUpdate)) {
|
||||
await setFieldsValue({
|
||||
...data.record,
|
||||
});
|
||||
openApiHeaderTable.dataSource = data.record.headersJson ? JSON.parse(data.record.headersJson) : [];
|
||||
openApiParamTable.dataSource = data.record.paramsJson ? JSON.parse(data.record.paramsJson) : [];
|
||||
bodyContent.value = data.record.body || '';
|
||||
responseExample.value = data.record.responseExample || '';
|
||||
responseFieldTable.dataSource = data.record.responseFieldsJson ? JSON.parse(data.record.responseFieldsJson) : [];
|
||||
} else {
|
||||
const requestUrlObj = await getGenPath({});
|
||||
await setFieldsValue({
|
||||
requestUrl: requestUrlObj.result,
|
||||
});
|
||||
}
|
||||
setProps({ disabled: !data?.showFooter });
|
||||
});
|
||||
|
||||
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(formDisabled) ? '编辑' : '详情'));
|
||||
|
||||
/** 解析白名单文本为条目数组 */
|
||||
function parseWhiteList(text: string): string[] {
|
||||
if (!text) return [];
|
||||
return text
|
||||
.split(/[,\n]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/** 整理白名单:去空行、去重、每行一个 */
|
||||
function formatWhiteList(model: any, field: string) {
|
||||
const items = parseWhiteList(model[field]);
|
||||
const unique = [...new Set(items)];
|
||||
model[field] = unique.join('\n');
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
await resetFields();
|
||||
activeTab.value = 'headers';
|
||||
openApiHeaderTable.dataSource = [];
|
||||
openApiParamTable.dataSource = [];
|
||||
responseFieldTable.dataSource = [];
|
||||
bodyContent.value = '';
|
||||
responseExample.value = '';
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
setDrawerProps({ confirmLoading: true });
|
||||
|
||||
// Collect JVxeTable data
|
||||
const headerData = await openApiHeader.value?.getTableData();
|
||||
const paramData = await openApiParam.value?.getTableData();
|
||||
const responseFieldData = await responseField.value?.getTableData();
|
||||
|
||||
const headersJson = headerData?.tableData?.length ? JSON.stringify(headerData.tableData) : null;
|
||||
const paramsJson = paramData?.tableData?.length ? JSON.stringify(paramData.tableData) : null;
|
||||
const responseFieldsJson = responseFieldData?.tableData?.length ? JSON.stringify(responseFieldData.tableData) : null;
|
||||
|
||||
// Validate body JSON
|
||||
if (bodyContent.value) {
|
||||
try {
|
||||
if (typeof JSON.parse(bodyContent.value) != 'object') {
|
||||
$message.createMessage.error('JSON格式化错误,请检查输入数据');
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
$message.createMessage.error('JSON格式化错误,请检查输入数据');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate response example JSON
|
||||
if (responseExample.value) {
|
||||
try {
|
||||
JSON.parse(responseExample.value);
|
||||
} catch (e) {
|
||||
$message.createMessage.error('响应示例JSON格式错误,请检查输入数据');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const submitValues = {
|
||||
...values,
|
||||
headersJson,
|
||||
paramsJson,
|
||||
body: bodyContent.value || null,
|
||||
responseExample: responseExample.value || null,
|
||||
responseFieldsJson,
|
||||
};
|
||||
|
||||
await saveOrUpdate(submitValues, isUpdate.value);
|
||||
closeDrawer();
|
||||
emit('success');
|
||||
} finally {
|
||||
setDrawerProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.ant-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-calendar-picker) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -46,7 +46,6 @@
|
||||
import { ref, computed, unref, reactive } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { JVxeTable } from '/@/components/jeecg/JVxeTable';
|
||||
import { useJvxeMethod } from '/@/hooks/system/useJvxeMethods.ts';
|
||||
import { formSchema, openApiHeaderJVxeColumns, openApiParamJVxeColumns } from '../OpenApi.data';
|
||||
import { saveOrUpdate, queryOpenApiHeader, queryOpenApiParam, getGenPath } from '../OpenApi.api';
|
||||
|
||||
@@ -131,7 +131,7 @@ export const generateMemoryByAppId = (params) => {
|
||||
url: Api.generateMemoryByAppId+'?variables='+ params.variables + '&memoryId='+ params.memoryId,
|
||||
adapter: 'fetch',
|
||||
responseType: 'stream',
|
||||
timeout: 5 * 60 * 1000,
|
||||
timeout: 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
isTransformResponse: false,
|
||||
|
||||
@@ -333,6 +333,7 @@
|
||||
initChartData(params.appId);
|
||||
} else {
|
||||
initChartData();
|
||||
appData.value.metadata = { izDraw: '1', defaultSelect: '0' }
|
||||
quickCommandData.value = [
|
||||
{ name: '请介绍一下JeecgBoot', descr: "请介绍一下JeecgBoot" },
|
||||
{ name: 'JEECG有哪些优势?', descr: "JEECG有哪些优势?" },
|
||||
|
||||
@@ -360,8 +360,8 @@
|
||||
const isThinking = ref<boolean>(false);
|
||||
//是否开启网络搜索
|
||||
const enableSearch = ref<boolean>(false);
|
||||
//是否显示网络搜索按钮(只有千问模型支持)
|
||||
const showWebSearch = ref<boolean>(false);
|
||||
//是否显示网络搜索按钮(默认显示)
|
||||
const showWebSearch = ref<boolean>(true);
|
||||
//模型provider信息
|
||||
const modelProvider = ref<string>('');
|
||||
//是否显示深度思考( 只有deepsee-reason支持 )
|
||||
@@ -565,7 +565,7 @@
|
||||
|
||||
// 停止响应
|
||||
const handleStop = () => {
|
||||
console.log('ai 聊天:::---停止响应');
|
||||
console.log('ai 聊天:::---停止响应, 当前loading:', loading.value, ', 调用栈:', new Error().stack?.split('\n').slice(1,4).join(' <- '));
|
||||
if (loading.value) {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -666,7 +666,7 @@
|
||||
params: param,
|
||||
adapter: 'fetch',
|
||||
responseType: 'stream',
|
||||
timeout: 5 * 60 * 1000,
|
||||
timeout: 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
isTransformResponse: false,
|
||||
@@ -1037,59 +1037,34 @@
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-03-12---for:【QQYUN-11555】聊天时要流式显示消息---
|
||||
let result = decoder.decode(value, { stream: true });
|
||||
result = buffer + result;
|
||||
const lines = result.split('\n\n');
|
||||
for (let line of lines) {
|
||||
if (line.startsWith('data:')) {
|
||||
let content = line.replace('data:', '').trim();
|
||||
if(!content){
|
||||
continue;
|
||||
}
|
||||
if(!content.endsWith('}')){
|
||||
buffer = buffer + content;
|
||||
continue;
|
||||
}
|
||||
buffer = "";
|
||||
try {
|
||||
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
if(content.indexOf(":::card:::") !== -1){
|
||||
content = content.replace(/\s+/g, '');
|
||||
}
|
||||
let parse = JSON.parse(content);
|
||||
await renderText(parse,conversationId,text,options).then((res)=>{
|
||||
text = res.returnText;
|
||||
conversationId = res.conversationId;
|
||||
});
|
||||
//update-end---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
} catch (error) {
|
||||
console.log('Error parsing update:', error);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-12---for:【QQYUN-11555】聊天时要流式显示消息---
|
||||
}else{
|
||||
if(!line){
|
||||
continue;
|
||||
}
|
||||
if(!line.endsWith('}')){
|
||||
buffer = buffer + line;
|
||||
continue;
|
||||
}
|
||||
buffer = "";
|
||||
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
try {
|
||||
if(line.indexOf(":::card:::") !== -1){
|
||||
line = line.replace(/\s+/g, '');
|
||||
}
|
||||
let parse = JSON.parse(line);
|
||||
await renderText(parse, conversationId, text, options).then((res) => {
|
||||
text = res.returnText;
|
||||
conversationId = res.conversationId;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Error parsing update:', error);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
buffer += result;
|
||||
// 按SSE协议用 \n\n 分割完整事件,最后一个元素可能不完整需保留在buffer中
|
||||
const parts = buffer.split('\n\n');
|
||||
buffer = parts.pop() || '';
|
||||
for (let part of parts) {
|
||||
if (!part || !part.trim()) {
|
||||
continue;
|
||||
}
|
||||
let content = part.startsWith('data:') ? part.replace('data:', '').trim() : part.trim();
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
try {
|
||||
if(content.indexOf(":::card:::") !== -1){
|
||||
content = content.replace(/\s+/g, '');
|
||||
}
|
||||
let parse = JSON.parse(content);
|
||||
await renderText(parse, conversationId, text, options).then((res) => {
|
||||
text = res.returnText;
|
||||
conversationId = res.conversationId;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('JSON解析失败, content长度:', content.length, ', error:', error);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-12---for:【QQYUN-11555】聊天时要流式显示消息---
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-11-05---for: 如果是断线重连并且文本为空,需要移出前面两条会话---
|
||||
if(!text && isReConnect && chatData.value.length >1){
|
||||
@@ -1118,7 +1093,7 @@
|
||||
const result = await defHttp.get({ url: '/airag/chat/receive/' + requestId ,
|
||||
adapter: 'fetch',
|
||||
responseType: 'stream',
|
||||
timeout: 5 * 60 * 1000
|
||||
timeout: 60 * 60 * 1000
|
||||
}, { isTransformResponse: false }).catch(async (err)=>{
|
||||
loading.value = false;
|
||||
localStorage.removeItem('chat_requestId_' + uuid.value);
|
||||
@@ -1240,29 +1215,28 @@
|
||||
|
||||
//是否显示绘图工具
|
||||
showDraw.value = metadata.izDraw === '1';
|
||||
//是否选中生成图片
|
||||
enableDraw.value = metadata.izDraw === '1';
|
||||
//是否选中生成图片(defaultSelect 为 0 时默认不选中)
|
||||
const defaultSelect = metadata.defaultSelect || metadata.izDraw;
|
||||
enableDraw.value = defaultSelect === '1';
|
||||
|
||||
drawModelId.value = metadata.drawModelId;
|
||||
|
||||
if (metadata && metadata.modelInfo) {
|
||||
modelProvider.value = metadata.modelInfo.provider || '';
|
||||
modelName.value = metadata.modelInfo.modelName || '';
|
||||
// 只有千问模型支持网络搜索
|
||||
showWebSearch.value = modelProvider.value === 'QWEN';
|
||||
showThink.value = modelName.value === 'deepseek-reasoner';
|
||||
} else {
|
||||
showWebSearch.value = false;
|
||||
showThink.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析模型信息失败', e);
|
||||
showWebSearch.value = false;
|
||||
showThink.value = false;
|
||||
enableDraw.value = false;
|
||||
}
|
||||
} else {
|
||||
showWebSearch.value = false;
|
||||
showThink.value = false;
|
||||
showDraw.value = false;
|
||||
enableDraw.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']" v-if="getText || (props.presetQuestion && props.presetQuestion.length>0)">
|
||||
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']" v-if="getText || props.error || (props.presetQuestion && props.presetQuestion.length>0)">
|
||||
<div class="avatar" v-if="showAvatar !== 'no'">
|
||||
<img v-if="inversion === 'user'" :src="avatar()" />
|
||||
<img v-else :src="getAiImg()" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="parsedText != ''" class="textWrap" :class="[inversion === 'user' ? 'self' : (isOnlyImage ? 'chatgpt-image' : 'chatgpt')]" ref="textRef">
|
||||
<div v-if="parsedText != '' || error" class="textWrap" :class="[inversion === 'user' ? 'self' : (isOnlyImage ? 'chatgpt-image' : 'chatgpt')]" ref="textRef">
|
||||
<div v-if="inversion != 'user'" :style="{ width: getIsMobile? screenWidth : 'auto' }">
|
||||
<div ref="markdownBodyRef" class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="parsedText" />
|
||||
<template v-if="showRefKnow">
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
enum Api {
|
||||
sqlPageExecute = '/airag/mcp/database/sqlPageExecute',
|
||||
sqlExportXls = '/airag/mcp/database/sqlExportXls',
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页执行 SQL 查询
|
||||
*/
|
||||
export function sqlPageExecute(params: { sql: string; dbSource?: string; pageNo: number; pageSize: number }) {
|
||||
return defHttp.post<Recordable>(
|
||||
{
|
||||
url: Api.sqlPageExecute,
|
||||
params: {
|
||||
sql: params.sql,
|
||||
dbSourceKey: params.dbSource || '',
|
||||
pageNo: params.pageNo,
|
||||
pageSize: params.pageSize,
|
||||
},
|
||||
},
|
||||
{ isTransformResponse: false }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出图表原始数据为 Excel
|
||||
*/
|
||||
export function sqlExportXls(params: { sql: string; dbSource?: string; columns?: Recordable }) {
|
||||
return defHttp.post(
|
||||
{
|
||||
url: Api.sqlExportXls,
|
||||
params: {
|
||||
sql: params.sql,
|
||||
dbSourceKey: params.dbSource || '',
|
||||
columns: params.columns || {},
|
||||
},
|
||||
responseType: 'blob',
|
||||
timeout: 5 * 60 * 1000,
|
||||
},
|
||||
{ isTransformResponse: false, isReturnNativeResponse: true }
|
||||
);
|
||||
}
|
||||
@@ -9,32 +9,42 @@
|
||||
<span v-else>模型返回的图表渲染格式不正确,请优化提示词或重新尝试。</span>
|
||||
</div>
|
||||
<div v-else class="ai-chat-chart__body">
|
||||
<!-- 折线图 -->
|
||||
<LineMulti v-if="resolvedType === 'line'" v-bind="lineProps"/>
|
||||
<!-- 柱状图 -->
|
||||
<BarMulti v-else-if="resolvedType === 'bar'" v-bind="barProps"/>
|
||||
<!-- 饼图 -->
|
||||
<Pie v-else-if="resolvedType === 'pie'" v-bind="pieProps"/>
|
||||
<!-- 多列柱状图 -->
|
||||
<BarMulti v-else-if="resolvedType === 'multibar'" v-bind="multiBarProps"/>
|
||||
<!-- 多行折线图 -->
|
||||
<LineMulti v-else-if="resolvedType === 'multiline'" v-bind="multiLineProps"/>
|
||||
<!-- 折柱图 -->
|
||||
<BarAndLine v-else-if="resolvedType === 'barline'" v-bind="barLineProps"/>
|
||||
<!-- 面积图 -->
|
||||
<SingleLine v-else-if="resolvedType === 'area'" v-bind="areaLineProps"/>
|
||||
<!-- 雷达图 -->
|
||||
<Radar v-else-if="resolvedType === 'radar'" v-bind="radarProps"/>
|
||||
<!-- 仪表盘 -->
|
||||
<Gauge v-else-if="resolvedType === 'gauge'" v-bind="gaugeProps"/>
|
||||
|
||||
<Tabs v-model:activeKey="activeTab" size="small" class="ai-chat-chart__tabs">
|
||||
<!-- 主图表 Tab -->
|
||||
<TabPane :key="resolvedType" :tab="getChartLabel(resolvedType)">
|
||||
<ChartBody :chartType="resolvedType" />
|
||||
</TabPane>
|
||||
<!-- 可替代图表 Tabs -->
|
||||
<TabPane v-for="alt in validAltTypes" :key="alt" :tab="getChartLabel(alt)">
|
||||
<ChartBody :chartType="alt" />
|
||||
</TabPane>
|
||||
<!-- 数据 Tab -->
|
||||
<TabPane v-if="hasSql" key="__data__" tab="数据">
|
||||
<div class="ai-chat-chart__data-toolbar">
|
||||
<a-button size="small" :loading="exportLoading" @click="handleExport">
|
||||
<template #icon><DownloadOutlined /></template>
|
||||
导出
|
||||
</a-button>
|
||||
</div>
|
||||
<BasicTable
|
||||
v-bind="tableBindings"
|
||||
:loading="tableLoading"
|
||||
size="small"
|
||||
class="ai-chat-chart__table"
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ChartType } from './types';
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
import type { BasicColumn } from '/@/components/Table';
|
||||
import { computed, defineComponent, h, ref, watch, watchEffect } from 'vue';
|
||||
import { Tabs, TabPane, message } from 'ant-design-vue';
|
||||
import { DownloadOutlined } from '@ant-design/icons-vue';
|
||||
import { BasicTable, useTable } from '/@/components/Table';
|
||||
import LineMulti from '/@/components/chart/LineMulti.vue';
|
||||
import BarMulti from '/@/components/chart/BarMulti.vue';
|
||||
import Pie from '/@/components/chart/Pie.vue';
|
||||
@@ -42,11 +52,12 @@ import Radar from '/@/components/chart/Radar.vue';
|
||||
import Gauge from '/@/components/chart/Gauge.vue';
|
||||
import BarAndLine from '/@/components/chart/BarAndLine.vue';
|
||||
import SingleLine from '/@/components/chart/SingleLine.vue';
|
||||
import { sqlPageExecute, sqlExportXls } from './ChartRender.api';
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 图表配置字符串,示例:
|
||||
* {"type":"bar","data":[{"x":"数据项1","y":100},{"x":"数据项2","y":80}]}
|
||||
* {"type":"bar","altTypes":["line","pie"],"data":[...],"sql":"SELECT ...","dbSource":"","columns":{"field1":"标题1"}}
|
||||
*/
|
||||
data: {
|
||||
type: String,
|
||||
@@ -58,6 +69,39 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
/** 图表类型中文名映射 */
|
||||
const chartTypeLabels: Record<string, string> = {
|
||||
bar: '柱状图',
|
||||
line: '折线图',
|
||||
pie: '饼图',
|
||||
radar: '雷达图',
|
||||
gauge: '仪表盘',
|
||||
barline: '折柱图',
|
||||
multibar: '多列柱状图',
|
||||
multiline: '多行折线图',
|
||||
area: '面积图',
|
||||
};
|
||||
|
||||
/** 获取图表类型的中文标签 */
|
||||
function getChartLabel(type: string): string {
|
||||
return chartTypeLabels[type] || type;
|
||||
}
|
||||
|
||||
/** 当前激活的 Tab */
|
||||
const activeTab = ref<string>('');
|
||||
/** 表格加载状态 */
|
||||
const tableLoading = ref(false);
|
||||
/** 导出加载状态 */
|
||||
const exportLoading = ref(false);
|
||||
/** 表格数据 */
|
||||
const tableData = ref<Recordable[]>([]);
|
||||
/** 表格列定义 */
|
||||
const tableColumns = ref<BasicColumn[]>([]);
|
||||
/** 数据总条数 */
|
||||
const tableTotal = ref(0);
|
||||
/** 是否已首次加载过数据 */
|
||||
const dataLoaded = ref(false);
|
||||
|
||||
/**
|
||||
* 解析失败或类型错误的提示文本。
|
||||
*/
|
||||
@@ -85,7 +129,6 @@ const parsedConfig = computed<Recordable>(() => {
|
||||
/**
|
||||
* 支持的图表类型集合。
|
||||
*/
|
||||
// 支持的类型覆盖常见图表,名称与提示词保持宽松映射
|
||||
const supportedTypes: ChartType[] = ['bar', 'line', 'pie', 'radar', 'gauge', 'barline', 'multibar', 'multiline', 'area'];
|
||||
|
||||
/**
|
||||
@@ -115,6 +158,25 @@ const resolvedType = computed<ChartType>(() => {
|
||||
return '';
|
||||
});
|
||||
|
||||
/** 解析可替代图表类型列表(过滤掉无效类型和主类型重复) */
|
||||
const validAltTypes = computed<ChartType[]>(() => {
|
||||
const altTypes = (parsedConfig.value as any).altTypes;
|
||||
if (!Array.isArray(altTypes)) {
|
||||
return [];
|
||||
}
|
||||
const primary = resolvedType.value;
|
||||
return altTypes
|
||||
.map((t: string) => String(t).toLowerCase() as ChartType)
|
||||
.filter((t: ChartType) => t !== primary && supportedTypes.includes(t));
|
||||
});
|
||||
|
||||
/** 初始化 activeTab 为主图表类型 */
|
||||
watchEffect(() => {
|
||||
if (resolvedType.value && !activeTab.value) {
|
||||
activeTab.value = resolvedType.value;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 当类型不被支持时,给出错误提示。
|
||||
*/
|
||||
@@ -149,6 +211,171 @@ const hasData = computed<boolean>(() => {
|
||||
return Array.isArray(rawData.value) && rawData.value.length > 0;
|
||||
});
|
||||
|
||||
/** 是否包含可查询的 SQL */
|
||||
const hasSql = computed<boolean>(() => {
|
||||
const sql = (parsedConfig.value as any).sql;
|
||||
return typeof sql === 'string' && sql.trim().length > 0;
|
||||
});
|
||||
|
||||
/** 从配置中提取 SQL */
|
||||
const chartSql = computed<string>(() => {
|
||||
return ((parsedConfig.value as any).sql || '').trim();
|
||||
});
|
||||
|
||||
/** 从配置中提取数据源 */
|
||||
const chartDbSource = computed<string>(() => {
|
||||
return ((parsedConfig.value as any).dbSource || '').trim();
|
||||
});
|
||||
|
||||
/** 从配置中提取列标题映射 */
|
||||
const chartColumns = computed<Recordable>(() => {
|
||||
return (parsedConfig.value as any).columns || {};
|
||||
});
|
||||
|
||||
/**
|
||||
* 使用 BasicTable 组件
|
||||
*/
|
||||
const [registerTable, { setPagination }] = useTable({
|
||||
columns: tableColumns,
|
||||
dataSource: tableData,
|
||||
pagination: {
|
||||
pageSize: 10,
|
||||
current: 1,
|
||||
total: 0,
|
||||
},
|
||||
showIndexColumn: false,
|
||||
canResize: false,
|
||||
bordered: true,
|
||||
onChange: handleTableChange,
|
||||
});
|
||||
|
||||
/** 表格绑定属性 */
|
||||
const tableBindings = computed(() => ({
|
||||
onRegister: registerTable,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 加载表格数据
|
||||
*/
|
||||
async function loadTableData(pageNo = 1, pageSize = 10) {
|
||||
if (!chartSql.value) {
|
||||
return;
|
||||
}
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const res = await sqlPageExecute({
|
||||
sql: chartSql.value,
|
||||
dbSource: chartDbSource.value,
|
||||
pageNo,
|
||||
pageSize,
|
||||
});
|
||||
if (res.success && res.result) {
|
||||
const { records = [], total = 0 } = res.result;
|
||||
tableData.value = records;
|
||||
tableTotal.value = total;
|
||||
// 根据返回数据动态生成列(使用 columns 映射中文标题)
|
||||
if (records.length > 0) {
|
||||
tableColumns.value = buildColumns(records[0]);
|
||||
}
|
||||
setPagination({
|
||||
current: pageNo,
|
||||
pageSize,
|
||||
total,
|
||||
});
|
||||
} else {
|
||||
tableData.value = [];
|
||||
tableTotal.value = 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载图表原始数据失败', e);
|
||||
tableData.value = [];
|
||||
tableTotal.value = 0;
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据数据行的 key 动态生成 BasicTable 列定义,优先使用 columns 映射的中文标题
|
||||
*/
|
||||
function buildColumns(row: Recordable): BasicColumn[] {
|
||||
const columnMap = chartColumns.value;
|
||||
return Object.keys(row).map((key) => ({
|
||||
title: columnMap[key] || key,
|
||||
dataIndex: key,
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格分页变更处理
|
||||
*/
|
||||
function handleTableChange(pagination: any) {
|
||||
loadTableData(pagination.current, pagination.pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出全部数据
|
||||
*/
|
||||
async function handleExport() {
|
||||
if (!chartSql.value) {
|
||||
return;
|
||||
}
|
||||
exportLoading.value = true;
|
||||
try {
|
||||
const response = await sqlExportXls({
|
||||
sql: chartSql.value,
|
||||
dbSource: chartDbSource.value,
|
||||
columns: chartColumns.value,
|
||||
});
|
||||
if (!response || !response.data) {
|
||||
message.warning('导出失败');
|
||||
return;
|
||||
}
|
||||
const data = response.data;
|
||||
// 检查是否为错误响应(JSON 格式)
|
||||
if (data.type && data.type.indexOf('json') !== -1) {
|
||||
const text = await data.text();
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
if (!json.success) {
|
||||
message.warning('导出失败:' + (json.message || '未知错误'));
|
||||
return;
|
||||
}
|
||||
} catch (_e) {
|
||||
// 非 JSON,继续下载
|
||||
}
|
||||
}
|
||||
const url = window.URL.createObjectURL(new Blob([data], { type: 'application/vnd.ms-excel' }));
|
||||
const link = document.createElement('a');
|
||||
link.style.display = 'none';
|
||||
link.href = url;
|
||||
link.setAttribute('download', '数据导出.xls');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
console.error('导出失败', e);
|
||||
message.error('导出失败');
|
||||
} finally {
|
||||
exportLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到数据 Tab 时首次加载
|
||||
*/
|
||||
watch(activeTab, (val) => {
|
||||
if (val === '__data__' && !dataLoaded.value && hasSql.value) {
|
||||
dataLoaded.value = true;
|
||||
loadTableData();
|
||||
}
|
||||
});
|
||||
|
||||
// ======================== 图表数据转换 ========================
|
||||
|
||||
/**
|
||||
* 将原始数据标准化为多序列图表所需的结构。
|
||||
*/
|
||||
@@ -201,7 +428,7 @@ function buildMultiSeriesItem(item: Recordable): Recordable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析系列名称,优先使用 series,其次使用 type,最后回退为“数据”。
|
||||
* 解析系列名称,优先使用 series,其次使用 type,最后回退为"数据"。
|
||||
*/
|
||||
function resolveSeriesName(item: Recordable): string {
|
||||
if (item && item.series !== undefined) {
|
||||
@@ -295,114 +522,84 @@ const barLineSeriesData = computed<Recordable[]>(() => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 折线图的渲染属性。
|
||||
*/
|
||||
const lineProps = computed(() => {
|
||||
return {
|
||||
type: 'line',
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: multiSeriesData.value,
|
||||
};
|
||||
});
|
||||
// ======================== 根据类型获取图表 props ========================
|
||||
|
||||
/**
|
||||
* 柱状图的渲染属性。
|
||||
* 根据图表类型返回对应的渲染属性
|
||||
*/
|
||||
const barProps = computed(() => {
|
||||
return {
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: multiSeriesData.value,
|
||||
};
|
||||
});
|
||||
function getChartPropsForType(type: ChartType): Recordable {
|
||||
switch (type) {
|
||||
case 'line':
|
||||
return { type: 'line', height: '360px', width: '100%', chartData: multiSeriesData.value };
|
||||
case 'bar':
|
||||
return { height: '360px', width: '100%', chartData: multiSeriesData.value };
|
||||
case 'pie':
|
||||
return { height: '360px', width: '100%', chartData: pieSeriesData.value };
|
||||
case 'multibar':
|
||||
return { height: '360px', width: '100%', chartData: multiSeriesData.value, option: parsedConfig.value.option || {} };
|
||||
case 'multiline':
|
||||
return { type: 'line', height: '360px', width: '100%', chartData: multiSeriesData.value, option: parsedConfig.value.option || {} };
|
||||
case 'barline':
|
||||
return { height: '360px', width: '100%', chartData: barLineSeriesData.value, customColor: (parsedConfig.value as any).colors || [], option: parsedConfig.value.option || {} };
|
||||
case 'area':
|
||||
return { type: 'line', height: '360px', width: '100%', chartData: areaSeriesData.value, option: { ...(parsedConfig.value.option || {}), areaStyle: {} } };
|
||||
case 'radar':
|
||||
return { height: '420px', width: '100%', chartData: radarSeriesData.value, option: parsedConfig.value.option || {} };
|
||||
case 'gauge':
|
||||
return { height: '360px', width: '100%', chartData: gaugeData.value, option: parsedConfig.value.option || {}, seriesColor: (parsedConfig.value as any).seriesColor || undefined };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 饼图的渲染属性。
|
||||
* 根据图表类型返回对应的组件
|
||||
*/
|
||||
const pieProps = computed(() => {
|
||||
return {
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: pieSeriesData.value,
|
||||
};
|
||||
});
|
||||
function getChartComponentForType(type: ChartType) {
|
||||
switch (type) {
|
||||
case 'line':
|
||||
return LineMulti;
|
||||
case 'bar':
|
||||
return BarMulti;
|
||||
case 'pie':
|
||||
return Pie;
|
||||
case 'multibar':
|
||||
return BarMulti;
|
||||
case 'multiline':
|
||||
return LineMulti;
|
||||
case 'barline':
|
||||
return BarAndLine;
|
||||
case 'area':
|
||||
return SingleLine;
|
||||
case 'radar':
|
||||
return Radar;
|
||||
case 'gauge':
|
||||
return Gauge;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多列柱状图配置,与 bar 相同但允许区分类型前缀。
|
||||
* 内部图表渲染组件,根据 chartType 动态渲染对应图表
|
||||
*/
|
||||
const multiBarProps = computed(() => {
|
||||
return {
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: multiSeriesData.value,
|
||||
option: parsedConfig.value.option || {},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 多行折线图配置。
|
||||
*/
|
||||
const multiLineProps = computed(() => {
|
||||
return {
|
||||
type: 'line',
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: multiSeriesData.value,
|
||||
option: parsedConfig.value.option || {},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 面积折线图配置,开启面积样式。
|
||||
*/
|
||||
const areaLineProps = computed(() => {
|
||||
return {
|
||||
type: 'line',
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: areaSeriesData.value,
|
||||
option: { ...(parsedConfig.value.option || {}), areaStyle: {} },
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 雷达图配置。
|
||||
*/
|
||||
const radarProps = computed(() => {
|
||||
return {
|
||||
height: '420px',
|
||||
width: '100%',
|
||||
chartData: radarSeriesData.value,
|
||||
option: parsedConfig.value.option || {},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 仪表盘配置。
|
||||
*/
|
||||
const gaugeProps = computed(() => {
|
||||
return {
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: gaugeData.value,
|
||||
option: parsedConfig.value.option || {},
|
||||
seriesColor: (parsedConfig.value as any).seriesColor || undefined,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 折柱图配置,支持自定义颜色。
|
||||
*/
|
||||
const barLineProps = computed(() => {
|
||||
return {
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: barLineSeriesData.value,
|
||||
customColor: (parsedConfig.value as any).colors || [],
|
||||
option: parsedConfig.value.option || {},
|
||||
};
|
||||
const ChartBody = defineComponent({
|
||||
props: {
|
||||
chartType: {
|
||||
type: String as () => ChartType,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(bodyProps) {
|
||||
return () => {
|
||||
const comp = getChartComponentForType(bodyProps.chartType as ChartType);
|
||||
if (!comp) {
|
||||
return null;
|
||||
}
|
||||
const chartProps = getChartPropsForType(bodyProps.chartType as ChartType);
|
||||
return h(comp, chartProps);
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -410,7 +607,7 @@ const barLineProps = computed(() => {
|
||||
|
||||
.ai-chat-chart {
|
||||
width: 100%;
|
||||
min-width: 360px;
|
||||
min-width: 420px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
@@ -424,4 +621,22 @@ const barLineProps = computed(() => {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.ai-chat-chart__tabs {
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-chat-chart__data-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ai-chat-chart__table {
|
||||
:deep(.ant-table-wrapper) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -232,6 +232,7 @@ function formatData(value: unknown): string {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.tool-exec-loading,
|
||||
@@ -371,6 +372,7 @@ function formatData(value: unknown): string {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 340px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,13 @@
|
||||
prologue: '你好,我是 AI绘图智能体。',
|
||||
presetQuestion: '[{"key":1,"descr":"请生成一张具有日本风格的动漫成年女孩。","update":true}, {"key":2,"descr":"请生成一幅中国神话故事中,手持武器的哪吒形象。","update":true}]',
|
||||
metadata:"{\"izDraw\":\"1\"}"
|
||||
},
|
||||
{
|
||||
id: '1993651187913981953',
|
||||
name: '商品导购',
|
||||
icon: 'https://minio.jeecg.com/otatest/shoppingGuide_1769754188966.png',
|
||||
prologue: '向要了解哪一款产品,我可以给你介绍。',
|
||||
presetQuestion: '[{"key":1,"sort":1,"descr":"HIP 0603T Series","update":false},{"key":2,"sort":2,"descr":"CHIP 1206HC Series","update":false},{"key":3,"sort":3,"descr":"BRICK 1032ST Series","update":true}]',
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
resetField();
|
||||
await getPromptList();
|
||||
setModalProps({
|
||||
title: '选择提示词',
|
||||
height: 600,
|
||||
bodyStyle: { padding: '24px' },
|
||||
});
|
||||
@@ -200,6 +201,8 @@
|
||||
*/
|
||||
async function handleOk() {
|
||||
if (selectedPrompt.value) {
|
||||
// select 传递完整对象,供调用方按需取用 id/name/content
|
||||
emit('select', selectedPrompt.value);
|
||||
emit('ok', selectedPrompt.value.content);
|
||||
} else {
|
||||
emit('ok');
|
||||
|
||||
@@ -428,6 +428,20 @@
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
<a-row>
|
||||
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol">
|
||||
<div style="display: flex;margin-top: 10px">
|
||||
<div style="margin-left: 2px">显示工具调用过程:</div>
|
||||
<a-switch
|
||||
v-model:checked="showToolProcessChecked"
|
||||
:disabled="isRelease"
|
||||
checked-children="开"
|
||||
un-checked-children="关"
|
||||
@change="handleShowToolProcessChange"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
<a-row v-if="izDrawChecked" class="mt-10">
|
||||
<a-col :span="24">
|
||||
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol" v-bind="validateInfos.drawModelId">
|
||||
@@ -622,6 +636,8 @@
|
||||
const multiSessionChecked = ref<boolean>(true);
|
||||
//开启会话能力
|
||||
const izDrawChecked = ref<boolean>(false);
|
||||
//显示工具调用过程
|
||||
const showToolProcessChecked = ref<boolean>(true);
|
||||
// 是否已发布
|
||||
const isRelease = ref<boolean>(false);
|
||||
//对话设置弹窗ref
|
||||
@@ -1208,6 +1224,11 @@
|
||||
}else{
|
||||
izDrawChecked.value = false;
|
||||
}
|
||||
if(metadata.value?.showToolProcess != null){
|
||||
showToolProcessChecked.value = metadata.value.showToolProcess === '1';
|
||||
}else{
|
||||
showToolProcessChecked.value = true;
|
||||
}
|
||||
if(metadata.value?.drawModelId){
|
||||
formState.drawModelId = metadata.value.drawModelId;
|
||||
}
|
||||
@@ -1611,6 +1632,20 @@
|
||||
}
|
||||
//================================================ end 开启绘画 ========================================================
|
||||
|
||||
//================================================ begin 显示工具调用过程 =========================================================
|
||||
/**
|
||||
* 显示工具调用过程开关回调
|
||||
*/
|
||||
function handleShowToolProcessChange(checked){
|
||||
if(checked){
|
||||
metadata.value.showToolProcess = "1";
|
||||
}else{
|
||||
metadata.value.showToolProcess = "0";
|
||||
}
|
||||
formState.metadata = JSON.stringify(metadata.value);
|
||||
}
|
||||
//================================================ end 显示工具调用过程 ========================================================
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
title,
|
||||
@@ -1705,6 +1740,8 @@
|
||||
izDrawChecked,
|
||||
handleDrawChange,
|
||||
handleDrawModelChange,
|
||||
showToolProcessChecked,
|
||||
handleShowToolProcessChange,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
import { Icon } from '/src/components/Icon';
|
||||
import { Button, Checkbox, Switch, Popconfirm } from 'ant-design-vue';
|
||||
import { JVxeTypes, JVxeColumn, JVxeTableInstance } from '/src/components/jeecg/JVxeTable/types';
|
||||
import { JVxeTable } from '/src/components/jeecg/JVxeTable';
|
||||
import { useMessage } from '/src/hooks/web/useMessage';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -47,7 +46,6 @@
|
||||
components: {
|
||||
BasicModal,
|
||||
Icon,
|
||||
JVxeTable,
|
||||
AButton: Button,
|
||||
ACheckbox: Checkbox,
|
||||
ASwitch: Switch,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { FormSchema } from '@/components/Form';
|
||||
|
||||
/**
|
||||
* AI换衣 - 生成图片表单
|
||||
*/
|
||||
export const clothImageFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'drawModelId',
|
||||
label: '模型',
|
||||
component: 'JDictSelectTag',
|
||||
required: true,
|
||||
helpMessage: ['1、需要选择已激活的图像模型', '2、当前推荐通义万象模型 (wan2.5-i2i-preview)', '3、建议上传清晰的模特图和服装图以获得最佳效果'],
|
||||
componentProps: {
|
||||
dictCode: "airag_model where model_type = 'IMAGE' and activate_flag = 1,name,id",
|
||||
placeholder: '请选择图像模型',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'modelImage',
|
||||
label: '模特图片',
|
||||
component: 'JImageUpload',
|
||||
required: true,
|
||||
componentProps: {
|
||||
fileMax: 1,
|
||||
text: '上传模特',
|
||||
},
|
||||
helpMessage: ['上传模特图片,建议使用全身照,正面清晰'],
|
||||
},
|
||||
|
||||
{
|
||||
field: 'clothUpload',
|
||||
label: '服装',
|
||||
slot: 'clothUpload',
|
||||
component: 'Input',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
field: 'userPrompt',
|
||||
label: '提示词',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
rows: 4,
|
||||
placeholder: '在此输入你的提示词,或使用示例快速填充',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* AI换衣 - 生成视频表单
|
||||
*/
|
||||
export const clothVideoFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'drawModelId',
|
||||
label: '模型',
|
||||
component: 'JDictSelectTag',
|
||||
required: true,
|
||||
helpMessage: ['1、需要选择已激活的视频模型', '2、建议选择支持图生视频的模型'],
|
||||
componentProps: {
|
||||
dictCode: "airag_model where model_type = 'VIDEO' and activate_flag = 1,name,id",
|
||||
placeholder: '请选择视频模型',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'modelImage',
|
||||
label: '模特图片',
|
||||
component: 'JImageUpload',
|
||||
componentProps: {
|
||||
fileMax: 1,
|
||||
text: '上传模特',
|
||||
},
|
||||
helpMessage: ['上传模特图片,建议使用全身照,正面清晰'],
|
||||
},
|
||||
{
|
||||
field: 'clothUpload',
|
||||
label: '',
|
||||
slot: 'clothUpload',
|
||||
component: 'Input',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
field: 'userPrompt',
|
||||
label: '自定义提示词',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
rows: 4,
|
||||
placeholder: '在此输入你的提示词,或使用下方示例快速填充',
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
372
jeecgboot-vue3/src/views/super/airag/aicloth/AiClothChange.less
Normal file
372
jeecgboot-vue3/src/views/super/airag/aicloth/AiClothChange.less
Normal file
@@ -0,0 +1,372 @@
|
||||
.ai-cloth-change-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 12px 16px 16px;
|
||||
background-color: #f0f2f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
// 示例按钮行(单行显示,pill 样式)
|
||||
.examples-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
padding: 8px 0 12px 0;
|
||||
overflow-x: auto; /* 小屏幕时可以横向滚动 */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.example-btn {
|
||||
border-radius: 20px !important;
|
||||
border: 1px solid #2b8fff !important;
|
||||
color: #2b8fff !important;
|
||||
background: #fff !important;
|
||||
padding: 6px 14px !important;
|
||||
height: 36px !important;
|
||||
line-height: 22px !important;
|
||||
box-shadow: none !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.example-btn:hover {
|
||||
background: rgba(43, 143, 255, 0.06) !important;
|
||||
border-color: #1a6fe6 !important;
|
||||
color: #1a6fe6 !important;
|
||||
}
|
||||
|
||||
//顶部标题区
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fff;
|
||||
padding: 16px 16px;
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
margin: 0;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.header-desc {
|
||||
font-size: 13px;
|
||||
color: #8f959e;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.cloth-change-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
//左侧配置面板
|
||||
.config-panel {
|
||||
width: 420px;
|
||||
min-width: 360px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.config-tabs {
|
||||
padding: 16px 20px 0;
|
||||
|
||||
:deep(.ant-tabs-nav::before) {
|
||||
border-bottom: none;
|
||||
}
|
||||
:deep(.ant-tabs-tab) {
|
||||
padding: 8px 0;
|
||||
margin: 0 24px 0 0;
|
||||
font-size: 15px;
|
||||
&.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: #00b96b;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
:deep(.ant-tabs-ink-bar) {
|
||||
background: #00b96b;
|
||||
}
|
||||
}
|
||||
|
||||
.form-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 20px 8px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 20px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
.gen-btn {
|
||||
height: 40px;
|
||||
padding: 0 40px;
|
||||
font-size: 15px;
|
||||
background: #00b96b;
|
||||
border-color: #00b96b;
|
||||
border-radius: 20px;
|
||||
&:hover {
|
||||
background: #00d97e;
|
||||
border-color: #00d97e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//单件服装上传
|
||||
.cloth-upload-area {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border: 1.5px dashed #d9d9d9;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: #fafafa;
|
||||
|
||||
.upload-text {
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
.uploaded-img-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.uploaded-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.img-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover .img-mask {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//多件服装上传
|
||||
.multi-cloth-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.cloth-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.cloth-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
flex: 1;
|
||||
border: 1.5px dashed #d9d9d9;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: #fafafa;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
.uploaded-img-box {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.uploaded-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.img-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover .img-mask {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//视频提示
|
||||
.ai-notice {
|
||||
margin-top: 10px;
|
||||
|
||||
:deep(.ant-alert) {
|
||||
font-size: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
:deep(.ant-alert-message) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
//区块
|
||||
.section-block {
|
||||
margin-top: 16px;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 状态
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #8f959e;
|
||||
p {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 结果展示
|
||||
.result-image-wrapper,
|
||||
.result-video-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.result-image,
|
||||
.result-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.hover-actions {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
border-radius: 8px;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
&:hover .hover-actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
//右侧预览面板
|
||||
.preview-panel {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
background: #f7f8fc;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
284
jeecgboot-vue3/src/views/super/airag/aicloth/AiClothChange.vue
Normal file
284
jeecgboot-vue3/src/views/super/airag/aicloth/AiClothChange.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div class="ai-cloth-change-page">
|
||||
<!-- 顶部标题区 -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">AI 换衣</h1>
|
||||
<div class="header-desc">将模特图片和服装图片上传,AI自动生成换衣效果</div>
|
||||
</div>
|
||||
|
||||
<div class="cloth-change-wrapper">
|
||||
<!-- 左侧配置面板 -->
|
||||
<div class="config-panel">
|
||||
<!-- 顶部 Tab:生成图片 / 生成视频 -->
|
||||
<div class="config-tabs">
|
||||
<a-tabs v-model:activeKey="genType" :tabBarStyle="{ margin: 0 }">
|
||||
<a-tab-pane key="image" tab="生成图片" />
|
||||
<a-tab-pane key="video" tab="生成视频" />
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
||||
<div class="form-scroll">
|
||||
<div class="examples-row" role="toolbar" aria-label="示例">
|
||||
<a-button class="example-btn" type="default" @click.prevent="useExample1">示例:换整体服装</a-button>
|
||||
<a-button class="example-btn" type="default" @click.prevent="useExample2">示例:上衣/裤子</a-button>
|
||||
</div>
|
||||
<!-- 模型选择 -->
|
||||
<BasicForm @register="registerForm">
|
||||
<!-- 上传区域(单一上传组件支持多张图片) -->
|
||||
<template #clothUpload>
|
||||
<div class="section-block">
|
||||
<JImageUpload v-model:value="clothUploads" :fileMax="2" text="上传服装" />
|
||||
</div>
|
||||
</template>
|
||||
</BasicForm>
|
||||
|
||||
<!-- 视频提示 -->
|
||||
<div v-if="genType === 'video'" class="ai-notice">
|
||||
<a-alert message="视频生成可能需要较长时间,请耐心等待~" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部生成按钮 -->
|
||||
<div class="action-bar">
|
||||
<a-button type="primary" size="large" block class="gen-btn" :loading="loading" @click="handleGenerate"> 生成 </a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧结果展示 -->
|
||||
<div class="preview-panel">
|
||||
<div class="preview-content">
|
||||
<div v-if="!generatedResult && !loading" class="empty-state">
|
||||
<Icon icon="ant-design:picture-outlined" size="64" color="#ccc" />
|
||||
<p>在左侧配置后点击「生成」</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-state">
|
||||
<a-spin size="large" :tip="`正在${genType === 'image' ? '生成图片' : '生成视频'},请稍候...`" />
|
||||
</div>
|
||||
|
||||
<!-- 图片结果 -->
|
||||
<div v-if="genType === 'image' && generatedResult && !loading" class="result-image-wrapper group">
|
||||
<img :src="generatedResult" class="result-image" alt="换衣结果" />
|
||||
<div class="hover-actions">
|
||||
<a-button type="primary" ghost @click="previewVisible = true"> <Icon icon="ant-design:eye-outlined" /> 预览 </a-button>
|
||||
<a-button type="primary" ghost @click="handleDownload"> <Icon icon="ant-design:download-outlined" /> 下载 </a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频结果 -->
|
||||
<div v-if="genType === 'video' && generatedResult && !loading" class="result-video-wrapper group">
|
||||
<video ref="videoRef" :src="generatedResult" controls class="result-video" />
|
||||
<div class="hover-actions">
|
||||
<a-button type="primary" ghost @click="handleDownload"> <Icon icon="ant-design:download-outlined" /> 下载 </a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ImageViewer v-if="previewVisible" :imageUrl="generatedResult" @hide="previewVisible = false" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { BasicForm, useForm, JImageUpload } from '@/components/Form';
|
||||
import { clothImageFormSchema, clothVideoFormSchema } from './AiClothChange.data';
|
||||
import ImageViewer from '../aiapp/chat/components/ImageViewer.vue';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { Icon } from '@/components/Icon';
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
|
||||
const TASK_ID_KEY = 'ai_cloth_task_id';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const { domainUrl } = useGlobSetting();
|
||||
|
||||
// 状态
|
||||
// 生成类型
|
||||
const genType = ref<'image' | 'video'>('image');
|
||||
|
||||
// 上传的服装图片(JImageUpload 返回逗号拼接的路径字符串)
|
||||
const clothUploads = ref<string>('');
|
||||
const loading = ref(false);
|
||||
const generatedResult = ref('');
|
||||
const previewVisible = ref(false);
|
||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||
let pollTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// 表单
|
||||
const [registerForm, { validate, resetSchema, getFieldsValue, setFieldsValue }] = useForm({
|
||||
schemas: clothImageFormSchema,
|
||||
showActionButtonGroup: false,
|
||||
wrapperCol: { span: 24 },
|
||||
labelCol: { span: 24 },
|
||||
});
|
||||
|
||||
// 不再区分单件/多件,提示词可由用户自定义
|
||||
|
||||
watch(genType, (val) => {
|
||||
generatedResult.value = '';
|
||||
resetSchema(val === 'image' ? clothImageFormSchema : clothVideoFormSchema);
|
||||
});
|
||||
|
||||
/**
|
||||
* 构建提示词
|
||||
* @param values
|
||||
*/
|
||||
function buildPrompt(values: any): string {
|
||||
// 若用户自定义了提示词则以用户输入为主(仍会在开头列出图的顺序说明)
|
||||
const userPrompt: string = (values.userPrompt || '').toString().trim();
|
||||
// 如果用户输入了提示词,则把用户提示放在后面,不生成自动 prompt 内容
|
||||
if (userPrompt) {
|
||||
return `用户提示:\n${userPrompt}`;
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
/** 轮询查询任务结果 */
|
||||
function startPolling(taskId: string) {
|
||||
const poll = () => {
|
||||
defHttp
|
||||
.get({ url: `/airag/chat/getAiPosterResult/${taskId}` }, { isTransformResponse: false })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
if (res.result === 'pending' || res.result === null) {
|
||||
pollTimer = setTimeout(poll, 3000);
|
||||
} else {
|
||||
const reg = /#\s*{\s*domainURL\s*}/g;
|
||||
generatedResult.value = (res.result as string).replace(reg, domainUrl + '/sys/common/static');
|
||||
loading.value = false;
|
||||
localStorage.removeItem(TASK_ID_KEY);
|
||||
createMessage.success(genType.value === 'image' ? '图片生成成功!' : '视频生成成功!');
|
||||
}
|
||||
} else {
|
||||
loading.value = false;
|
||||
localStorage.removeItem(TASK_ID_KEY);
|
||||
createMessage.warning(res.message || '生成失败!');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
pollTimer = setTimeout(poll, 3000);
|
||||
});
|
||||
};
|
||||
poll();
|
||||
}
|
||||
//update-end---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
|
||||
/**
|
||||
* 生成
|
||||
*/
|
||||
async function handleGenerate() {
|
||||
// 校验服装图(从 clothUploads 解析)
|
||||
const validCloth = (clothUploads.value || '').split(',').filter(Boolean);
|
||||
if (validCloth.length === 0) {
|
||||
createMessage.warning('请上传服装图片');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const values = await validate();
|
||||
loading.value = true;
|
||||
generatedResult.value = '';
|
||||
|
||||
// 组装图片 URL(模特 + 服装),按顺序:模特图为图一,后面依次为服装图片(图二/图三)
|
||||
const imgUrls: string[] = [];
|
||||
if (values.modelImage) {
|
||||
const modelFirst = (values.modelImage || '').toString().split(',')[0];
|
||||
if (modelFirst) imgUrls.push(modelFirst);
|
||||
}
|
||||
imgUrls.push(...validCloth);
|
||||
|
||||
if (genType.value === 'image') {
|
||||
values.imageSize = '720*1280';
|
||||
} else {
|
||||
createMessage.info('敬请期待');
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
const prompt = buildPrompt(values);
|
||||
|
||||
const params: Record<string, any> = {
|
||||
drawModelId: values.drawModelId,
|
||||
//update-begin---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
content: prompt,
|
||||
imageUrl: imgUrls.join(','),
|
||||
//update-end---author:wangshuai ---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
type: genType.value === 'image' ? 'cloth_image' : 'cloth_video',
|
||||
imageSize: values.imageSize,
|
||||
};
|
||||
|
||||
//update-begin---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
const res = await defHttp.post(
|
||||
{ url: '/airag/chat/genAiPosterAsync', params },
|
||||
{ isTransformResponse: false },
|
||||
);
|
||||
if (res.success && res.result) {
|
||||
const taskId = res.result as string;
|
||||
localStorage.setItem(TASK_ID_KEY, taskId);
|
||||
startPolling(taskId);
|
||||
} else {
|
||||
loading.value = false;
|
||||
createMessage.warning('提交任务失败!');
|
||||
}
|
||||
//update-end---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
} catch {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
onMounted(() => {
|
||||
const savedTaskId = localStorage.getItem(TASK_ID_KEY);
|
||||
if (savedTaskId) {
|
||||
loading.value = true;
|
||||
generatedResult.value = '';
|
||||
startPolling(savedTaskId);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollTimer) {
|
||||
clearTimeout(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
});
|
||||
//update-end---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
|
||||
/**
|
||||
* 下载
|
||||
*/
|
||||
function handleDownload() {
|
||||
if (!generatedResult.value) {
|
||||
return;
|
||||
}
|
||||
const a = document.createElement('a');
|
||||
a.href = generatedResult.value;
|
||||
a.download = `ai-cloth-change-${Date.now()}.${genType.value === 'image' ? 'jpg' : 'mp4'}`;
|
||||
a.target = '_blank';
|
||||
a.click();
|
||||
}
|
||||
|
||||
// 示例提示词操作
|
||||
function useExample1() {
|
||||
const example =
|
||||
'图像映射: 图一=模特; 图二=服装素材。任务: 将图二整体替换到图一身上,保持模特面部与姿态不变,服装贴合自然,光影一致;服装贴合自然,光影一致,风格写实高清。请输出高质量合成图';
|
||||
const modelImage = 'https://jeecgdev.oss-cn-beijing.aliyuncs.com/upload/test/model_1772695749704.jpg';
|
||||
clothUploads.value = 'https://jeecgdev.oss-cn-beijing.aliyuncs.com/upload/test/dress_1772700962866.jpg';
|
||||
setFieldsValue({ userPrompt: example, modelImage: modelImage });
|
||||
}
|
||||
|
||||
function useExample2() {
|
||||
const example =
|
||||
'图像映射: 图一=模特; 图二=上衣素材; 图三=下装素材(可选)。任务: 仅将图二的上衣替换到图一上半身(胸部/肩部/袖口),严格不修改面部或下半身; 对齐按肩线/胸围并融合光照; 风格写实高清。请输出高质量合成图';
|
||||
const modelImage = 'https://jeecgdev.oss-cn-beijing.aliyuncs.com/upload/test/model_1772695749704.jpg';
|
||||
clothUploads.value =
|
||||
'https://jeecgdev.oss-cn-beijing.aliyuncs.com/upload/test/jacket_1772701290346.jpg,https://jeecgdev.oss-cn-beijing.aliyuncs.com/upload/test/pants_1772701320192.jpg';
|
||||
setFieldsValue({ userPrompt: example, modelImage: modelImage });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import 'AiClothChange.less';
|
||||
</style>
|
||||
@@ -70,7 +70,98 @@ export const formSchema: FormSchema[] = [
|
||||
type: 'radioButton',
|
||||
},
|
||||
defaultValue: 'knowledge',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '分段策略',
|
||||
field: 'enableSegment',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
ifShow: ({ values }) => values.type !== 'memory',
|
||||
componentProps: {
|
||||
checkedChildren: '开启',
|
||||
unCheckedChildren: '关闭',
|
||||
},
|
||||
helpMessage: '开启后,知识库里面的文档默认使用该分段策略;文档也可单独配置自己的分段策略',
|
||||
},
|
||||
{
|
||||
label: '分段模式',
|
||||
field: 'segmentStrategy',
|
||||
component: 'RadioGroup',
|
||||
defaultValue: 'auto',
|
||||
ifShow: ({ values }) => values.type !== 'memory' && values.enableSegment === true,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '自动分段与清洗', value: 'auto' },
|
||||
{ label: '自定义', value: 'custom' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '分段标识符',
|
||||
field: 'separator',
|
||||
component: 'Select',
|
||||
defaultValue: '\\n',
|
||||
required: true,
|
||||
ifShow: ({ values }) => values.type !== 'memory' && values.enableSegment === true && values.segmentStrategy === 'custom',
|
||||
componentProps: {
|
||||
getPopupContainer: () => document.body,
|
||||
options: [
|
||||
{ label: '换行', value: '\\n' },
|
||||
{ label: '2个换行', value: '\\n\\n' },
|
||||
{ label: '中文句号', value: '。' },
|
||||
{ label: '中文叹号', value: '!' },
|
||||
{ label: '中文问号', value: '?' },
|
||||
{ label: '英文句号', value: '.' },
|
||||
{ label: '英文叹号', value: '!' },
|
||||
{ label: '英文问号', value: '?' },
|
||||
{ label: '自定义', value: 'custom' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '自定义分隔符',
|
||||
field: 'customSeparator',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
ifShow: ({ values }) => values.type !== 'memory' && values.enableSegment === true && values.separator === 'custom' && values.segmentStrategy !== 'auto',
|
||||
},
|
||||
{
|
||||
label: '分段最大长度',
|
||||
field: 'maxSegment',
|
||||
component: 'InputNumber',
|
||||
defaultValue: 800,
|
||||
required: true,
|
||||
ifShow: ({ values }) => values.type !== 'memory' && values.enableSegment === true,
|
||||
componentProps: {
|
||||
min: 100,
|
||||
max: 5000,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '分段重叠度%',
|
||||
field: 'overlap',
|
||||
component: 'InputNumber',
|
||||
defaultValue: 10,
|
||||
required: true,
|
||||
ifShow: ({ values }) => values.type !== 'memory' && values.enableSegment === true,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
max: 90,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '文本预处理规则',
|
||||
field: 'textRules',
|
||||
component: 'CheckboxGroup',
|
||||
defaultValue: [],
|
||||
ifShow: ({ values }) => values.type !== 'memory' && values.enableSegment === true && values.segmentStrategy === 'custom',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '替换掉连续的空格、换行符和制表符', value: 'cleanSpaces' },
|
||||
{ label: '删除所有 URL 和电子邮箱地址', value: 'removeUrlsEmails' },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
//文档文本表单
|
||||
@@ -135,5 +226,105 @@ export const docTextSchema: FormSchema[] = [
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '网页地址',
|
||||
field: 'website',
|
||||
rules: [
|
||||
{ required: true, message: '请输入网页URL' },
|
||||
{ pattern: /^https?:\/\//, message: '请输入正确的网页地址,以http://或https://开头' },
|
||||
],
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入网页URL,例如:https://help.jeecg.com/',
|
||||
},
|
||||
ifShow:({ values })=>{
|
||||
if(values.type === 'web'){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 分段策略表单
|
||||
*/
|
||||
export const docSegmentSchema: FormSchema[] = [
|
||||
{
|
||||
label: '分段策略',
|
||||
field: 'segmentStrategy',
|
||||
component: 'RadioGroup',
|
||||
defaultValue: 'auto',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '自动分段与清洗', value: 'auto' },
|
||||
{ label: '自定义', value: 'custom' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '分段标识符',
|
||||
field: 'separator',
|
||||
component: 'Select',
|
||||
defaultValue: '\\n',
|
||||
required: true,
|
||||
ifShow: ({ values }) => values.segmentStrategy === 'custom',
|
||||
componentProps: {
|
||||
getPopupContainer: () => document.body,
|
||||
options: [
|
||||
{ label: '换行', value: '\\n' },
|
||||
{ label: '2个换行', value: '\\n\\n' },
|
||||
{ label: '中文句号', value: '。' },
|
||||
{ label: '中文叹号', value: '!' },
|
||||
{ label: '中文问号', value: '?' },
|
||||
{ label: '英文句号', value: '.' },
|
||||
{ label: '英文叹号', value: '!' },
|
||||
{ label: '英文问号', value: '?' },
|
||||
{ label: '自定义', value: 'custom' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
field: 'customSeparator',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
ifShow: ({ values }) => values.separator === 'custom' && values.segmentStrategy !== 'auto',
|
||||
},
|
||||
{
|
||||
label: '分段最大长度',
|
||||
field: 'maxSegment',
|
||||
component: 'InputNumber',
|
||||
defaultValue: 800,
|
||||
required: true,
|
||||
componentProps: {
|
||||
min: 100,
|
||||
max: 5000,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '分段重叠度%',
|
||||
field: 'overlap',
|
||||
component: 'InputNumber',
|
||||
defaultValue: 10,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
max: 90,
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: '文本预处理规则',
|
||||
field: 'textRules',
|
||||
component: 'CheckboxGroup',
|
||||
defaultValue: [],
|
||||
ifShow: ({ values }) => values.segmentStrategy === 'custom',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '替换掉连续的空格、换行符和制表符', value: 'cleanSpaces' },
|
||||
{ label: '删除所有 URL 和电子邮箱地址', value: 'removeUrlsEmails' },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col v-if="knowledgeList && knowledgeList.length>0" :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24" v-for="item in knowledgeList">
|
||||
<a-card class="knowledge-card pointer" @click="handleDocClick(item.id, item.type)">
|
||||
<a-card class="knowledge-card pointer" @click="handleDocClick(item)">
|
||||
<div class="knowledge-header">
|
||||
<div class="flex">
|
||||
<img class="header-img" src="./icon/knowledge.png" />
|
||||
@@ -273,8 +273,10 @@
|
||||
* @param id
|
||||
* @param type
|
||||
*/
|
||||
function handleDocClick(id, type) {
|
||||
openDocModal(true, { id, type });
|
||||
//update-begin---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
function handleDocClick(item) {
|
||||
openDocModal(true, { id: item.id, type: item.type, knowledgeMetadata: item.metadata });
|
||||
//update-end---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
showActionButtonGroup: false,
|
||||
layout: 'vertical',
|
||||
wrapperCol: { span: 24 },
|
||||
labelCol: { span: 24 },
|
||||
});
|
||||
|
||||
//注册modal
|
||||
@@ -57,10 +58,27 @@
|
||||
title.value = isUpdate.value ? '编辑知识库' : '创建知识库';
|
||||
if (unref(isUpdate)) {
|
||||
let values = await queryById({ id: data.id });
|
||||
//update-begin---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
let record = { ...values.result };
|
||||
// 解析 metadata 中的分段策略
|
||||
if (record.metadata) {
|
||||
try {
|
||||
const meta = JSON.parse(record.metadata);
|
||||
const hasSegment = !!(meta.enableSegment || meta.segmentStrategy || meta.maxSegment);
|
||||
record.enableSegment = hasSegment;
|
||||
if (hasSegment) {
|
||||
record.segmentStrategy = meta.segmentStrategy || 'auto';
|
||||
record.maxSegment = meta.maxSegment;
|
||||
record.overlap = meta.overlap;
|
||||
record.separator = meta.separator;
|
||||
record.customSeparator = meta.customSeparator;
|
||||
record.textRules = meta.textRules;
|
||||
}
|
||||
} catch (_e) {}
|
||||
}
|
||||
//表单赋值
|
||||
await setFieldsValue({
|
||||
...values.result,
|
||||
});
|
||||
await setFieldsValue(record);
|
||||
//update-end---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
}
|
||||
setModalProps({ minHeight: 500, bodyStyle: { padding: '10px' } });
|
||||
});
|
||||
@@ -72,10 +90,32 @@
|
||||
try {
|
||||
setModalProps({ confirmLoading: true });
|
||||
let values = await validate();
|
||||
if (!unref(isUpdate)) {
|
||||
await saveKnowledge(values);
|
||||
//update-begin---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
// 将分段策略字段打包到 metadata
|
||||
const { enableSegment, segmentStrategy, separator, customSeparator, maxSegment, overlap, textRules, ...rest } = values;
|
||||
let params: any = { ...rest };
|
||||
if (enableSegment) {
|
||||
const meta: any = {
|
||||
enableSegment: true,
|
||||
segmentStrategy: segmentStrategy || 'auto',
|
||||
maxSegment,
|
||||
overlap,
|
||||
};
|
||||
if (segmentStrategy === 'custom') {
|
||||
meta.separator = separator;
|
||||
meta.customSeparator = customSeparator;
|
||||
meta.textRules = textRules;
|
||||
}
|
||||
params.metadata = JSON.stringify(meta);
|
||||
} else {
|
||||
await editKnowledge(values);
|
||||
params.metadata = null;
|
||||
}
|
||||
|
||||
if (!unref(isUpdate)) {
|
||||
await saveKnowledge(params);
|
||||
} else {
|
||||
await editKnowledge(params);
|
||||
//update-end---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
}
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
<div class="add-knowledge-doc" @click="handleCreateUpload">
|
||||
<Icon icon="ant-design:cloud-upload-outlined" size="13"></Icon><span>文件上传</span>
|
||||
</div>
|
||||
<div class="add-knowledge-doc" @click="handleCreateWeb">
|
||||
<Icon icon="ant-design:global-outlined" size="13"></Icon><span>网页录入</span>
|
||||
</div>
|
||||
<div class="add-knowledge-doc">
|
||||
<a-upload
|
||||
accept=".zip"
|
||||
@@ -98,6 +101,7 @@
|
||||
<div class="knowledge-header">
|
||||
<div class="header-text flex">
|
||||
<Icon v-if="item.type==='text'" icon="ant-design:file-text-outlined" size="32" color="#00a7d0"></Icon>
|
||||
<Icon v-if="item.type==='web'" icon="ant-design:global-outlined" size="32" color="#1890ff"></Icon>
|
||||
<Icon v-if="item.type==='file' && getFileSuffix(item.metadata) === 'pdf'" icon="ant-design:file-pdf-outlined" size="32" color="rgb(211, 47, 47)"></Icon>
|
||||
<Icon v-if="item.type==='file' && getFileSuffix(item.metadata) === 'docx'" icon="ant-design:file-word-outlined" size="32" color="rgb(68, 138, 255)"></Icon>
|
||||
<Icon v-if="item.type==='file' && getFileSuffix(item.metadata) === 'pptx'" icon="ant-design:file-ppt-outlined" size="32" color="rgb(245, 124, 0)"></Icon>
|
||||
@@ -360,6 +364,8 @@
|
||||
const [docTextRegister, { openModal: docTextOpenModal }] = useModal();
|
||||
const [docTextDescRegister, { openModal: docTextDescOpenModal }] = useModal();
|
||||
const type = ref<string>('');
|
||||
// 知识库的分段策略 metadata
|
||||
const knowledgeMetadata = ref<string>('');
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
knowledgeId.value = data.id;
|
||||
@@ -368,6 +374,7 @@
|
||||
spinning.value = false;
|
||||
notHit.value = false;
|
||||
type.value = data.type;
|
||||
knowledgeMetadata.value = data.knowledgeMetadata || '';
|
||||
await reload();
|
||||
setModalProps({ confirmLoading: false });
|
||||
});
|
||||
@@ -403,22 +410,27 @@
|
||||
* 手工录入文本
|
||||
*/
|
||||
function handleCreateText() {
|
||||
docTextOpenModal(true, { knowledgeId: knowledgeId.value, type: "text" });
|
||||
//update-begin---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
docTextOpenModal(true, { knowledgeId: knowledgeId.value, type: "text", knowledgeMetadata: knowledgeMetadata.value, knowledgeType: type.value });
|
||||
//update-end---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
*/
|
||||
function handleCreateUpload() {
|
||||
console.log("11111111111")
|
||||
docTextOpenModal(true, { knowledgeId: knowledgeId.value, type: "file" });
|
||||
//update-begin---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
docTextOpenModal(true, { knowledgeId: knowledgeId.value, type: "file", knowledgeMetadata: knowledgeMetadata.value, knowledgeType: type.value });
|
||||
//update-end---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
}
|
||||
|
||||
/**
|
||||
* web网络地址
|
||||
*/
|
||||
function handleCreateWeb() {
|
||||
createMessage.warning('功能正在完善中....');
|
||||
//update-begin---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
docTextOpenModal(true, { knowledgeId: knowledgeId.value, type: "web", knowledgeMetadata: knowledgeMetadata.value, knowledgeType: type.value });
|
||||
//update-end---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -432,10 +444,12 @@
|
||||
}
|
||||
|
||||
|
||||
if (record.type === 'text' || record.type === 'file') {
|
||||
if (record.type === 'text' || record.type === 'file' || record.type === 'web') {
|
||||
docTextOpenModal(true, {
|
||||
record,
|
||||
isUpdate: true,
|
||||
knowledgeMetadata: knowledgeMetadata.value,
|
||||
knowledgeType: type.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -719,6 +733,7 @@
|
||||
handleCreateText,
|
||||
beforeUpload,
|
||||
handleCreateUpload,
|
||||
handleCreateWeb,
|
||||
handleSuccess,
|
||||
contentStyle,
|
||||
siderStyle,
|
||||
@@ -890,7 +905,7 @@
|
||||
margin-bottom: 20px;
|
||||
display: inline-flex;
|
||||
font-size: 16px;
|
||||
height: 166px;
|
||||
height: 196px;
|
||||
width: calc(100% - 20px);
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
@@ -906,7 +921,7 @@
|
||||
border-radius: 10px;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
height: 166px;
|
||||
height: 196px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px #e6e6e6;
|
||||
|
||||
@@ -2,21 +2,52 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicModal destroyOnClose @register="registerModal" width="600px" :title="title" @ok="handleOk" @cancel="handleCancel">
|
||||
<BasicForm @register="registerForm"></BasicForm>
|
||||
<div v-show="currentStep === 0">
|
||||
<BasicForm @register="registerForm"></BasicForm>
|
||||
<div v-if="showWebContent" class="web-content-preview">
|
||||
<div class="web-content-label">解析内容(只读)</div>
|
||||
<div class="web-content-body">
|
||||
<pre>{{ webContentText }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="currentStep === 1">
|
||||
<!-- 知识库有默认分段策略时,显示选择来源 -->
|
||||
<div v-if="knowledgeDefaultSegment" style="margin-bottom: 16px">
|
||||
<div style="margin-bottom: 8px; font-weight: 500; color: rgba(0,0,0,0.85)">分段策略来源</div>
|
||||
<a-radio-group v-model:value="useKnowledgeDefault" button-style="solid">
|
||||
<a-radio-button value="default">使用知识库默认</a-radio-button>
|
||||
<a-radio-button value="custom">自定义</a-radio-button>
|
||||
</a-radio-group>
|
||||
<div style="margin-top: 6px; font-size: 12px; color: rgba(0,0,0,0.45)">
|
||||
<span v-if="useKnowledgeDefault === 'default'">直接保存,文档将使用知识库配置的分段策略</span>
|
||||
<span v-else>忽略知识库默认策略,为该文档单独配置分段参数</span>
|
||||
</div>
|
||||
<!-- 只读展示知识库默认策略 -->
|
||||
<div v-if="useKnowledgeDefault === 'default'" class="default-segment-info">
|
||||
<a-descriptions :column="2" size="small" bordered style="margin-top: 12px">
|
||||
<a-descriptions-item label="分段模式">{{ knowledgeDefaultSegment.segmentStrategy === 'custom' ? '自定义' : '自动分段与清洗' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="最大长度">{{ knowledgeDefaultSegment.maxSegment }}</a-descriptions-item>
|
||||
<a-descriptions-item label="重叠度%">{{ knowledgeDefaultSegment.overlap }}</a-descriptions-item>
|
||||
<a-descriptions-item v-if="knowledgeDefaultSegment.segmentStrategy === 'custom'" label="分段标识符">{{ knowledgeDefaultSegment.separator === 'custom' ? knowledgeDefaultSegment.customSeparator : knowledgeDefaultSegment.separator }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
<BasicForm v-show="!knowledgeDefaultSegment || useKnowledgeDefault === 'custom'" @register="registerSegmentForm"></BasicForm>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, unref } from 'vue';
|
||||
import { ref, unref, computed } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModal, useModalInner } from '@/components/Modal';
|
||||
import { useModalInner } from '@/components/Modal';
|
||||
|
||||
import BasicForm from '@/components/Form/src/BasicForm.vue';
|
||||
import { useForm } from '@/components/Form';
|
||||
import { docTextSchema } from '../AiKnowledgeBase.data';
|
||||
import { knowledgeSaveDoc, queryById } from '../AiKnowledgeBase.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { docSegmentSchema, docTextSchema } from '../AiKnowledgeBase.data';
|
||||
import { knowledgeSaveDoc } from '../AiKnowledgeBase.api';
|
||||
|
||||
export default {
|
||||
name: 'AiragKnowledgeDocModal',
|
||||
@@ -27,11 +58,25 @@
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const title = ref<string>('创建知识库');
|
||||
const currentStep = ref(0);
|
||||
const step1Values = ref({});
|
||||
//自定义分词的数据
|
||||
const segmentMetadataRef = ref<any>({});
|
||||
// 知识库默认分段策略(有值表示知识库配置了默认分段)
|
||||
const knowledgeDefaultSegment = ref<any>(null);
|
||||
// 分段策略来源:'default' 使用知识库默认,'custom' 自定义
|
||||
const useKnowledgeDefault = ref<'default' | 'custom'>('default');
|
||||
// 知识库类型:'knowledge' | 'memory'
|
||||
const knowledgeType = ref<string>('knowledge');
|
||||
|
||||
//保存或修改
|
||||
const isUpdate = ref<boolean>(false);
|
||||
//知识库id
|
||||
const knowledgeId = ref<string>();
|
||||
//网页解析内容(只读展示)
|
||||
const webContentText = ref<string>('');
|
||||
const docType = ref<string>('');
|
||||
const showWebContent = computed(() => docType.value === 'web' && isUpdate.value && webContentText.value);
|
||||
//表单配置
|
||||
const [registerForm, { resetFields, setFieldsValue, validate, clearValidate, updateSchema }] = useForm({
|
||||
schemas: docTextSchema,
|
||||
@@ -40,24 +85,92 @@
|
||||
wrapperCol: { span: 24 },
|
||||
});
|
||||
|
||||
const [registerSegmentForm, { resetFields: resetSegmentFields, validate: validateSegment, setFieldsValue: setSegmentFieldsValue }] = useForm({
|
||||
schemas: docSegmentSchema,
|
||||
showActionButtonGroup: false,
|
||||
layout: 'vertical',
|
||||
wrapperCol: { span: 24 },
|
||||
labelCol: { span: 24 },
|
||||
});
|
||||
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
//重置表单
|
||||
await resetFields();
|
||||
setModalProps({ confirmLoading: false });
|
||||
await resetSegmentFields();
|
||||
currentStep.value = 0;
|
||||
webContentText.value = '';
|
||||
docType.value = '';
|
||||
knowledgeType.value = data?.knowledgeType || 'knowledge';
|
||||
setModalProps({ confirmLoading: false, okText: knowledgeType.value === 'memory' ? '保存' : '下一步' });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
title.value = isUpdate.value ? '编辑文档' : '创建文档';
|
||||
|
||||
//update-begin---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
// 解析知识库默认分段策略
|
||||
knowledgeDefaultSegment.value = null;
|
||||
useKnowledgeDefault.value = 'default';
|
||||
if (data?.knowledgeMetadata) {
|
||||
try {
|
||||
const kmeta = JSON.parse(data.knowledgeMetadata);
|
||||
if (kmeta.enableSegment) {
|
||||
knowledgeDefaultSegment.value = kmeta;
|
||||
}
|
||||
} catch (_e) {}
|
||||
}
|
||||
//update-end---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
|
||||
if (unref(isUpdate)) {
|
||||
docType.value = data.record.type || '';
|
||||
if(data.record.type === 'file' && data.record.metadata){
|
||||
data.record.filePath = JSON.parse(data.record.metadata).filePath;
|
||||
}
|
||||
if(data.record.type === 'web' && data.record.metadata){
|
||||
data.record.website = JSON.parse(data.record.metadata).website;
|
||||
}
|
||||
if(data.record.type === 'web' && data.record.content){
|
||||
webContentText.value = data.record.content;
|
||||
}
|
||||
//表单赋值
|
||||
await setFieldsValue({
|
||||
...data.record,
|
||||
});
|
||||
|
||||
// 解析metadata并准备给第二步表单
|
||||
if (data.record.metadata) {
|
||||
const meta = JSON.parse(data.record.metadata);
|
||||
//update-begin---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
// 如果文档保存时使用了知识库默认策略,回显时恢复选项
|
||||
// 兼容老数据:若知识库已不存在默认分段策略,则降级为自定义(展示默认值)
|
||||
if (meta.useKnowledgeDefault && knowledgeDefaultSegment.value) {
|
||||
useKnowledgeDefault.value = 'default';
|
||||
} else {
|
||||
useKnowledgeDefault.value = 'custom';
|
||||
// update-begin--author:wangshuai--date:2026-04-09--for:【issue/9418】AI知识库上传文件太大向量化失败
|
||||
let strategy = meta.segmentStrategy || (meta.separator ? 'custom' : 'auto');
|
||||
// update-end--author:wangshuai--date:2026-04-09--for:【issue/9418】AI知识库上传文件太大向量化失败
|
||||
segmentMetadataRef.value = {
|
||||
segmentStrategy: strategy,
|
||||
maxSegment: meta.maxSegment,
|
||||
overlap: meta.overlap,
|
||||
textRules: meta.textRules,
|
||||
separator: meta.separator,
|
||||
preprocessingRules: meta.preprocessingRules,
|
||||
customSeparator: strategy === 'custom' ? meta.customSeparator : '',
|
||||
};
|
||||
}
|
||||
//update-end---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
} else {
|
||||
useKnowledgeDefault.value = knowledgeDefaultSegment.value ? 'default' : 'custom';
|
||||
segmentMetadataRef.value = { segmentStrategy: 'auto', separator: '\\n', maxSegment: 800, overlap: 10 };
|
||||
}
|
||||
} else {
|
||||
knowledgeId.value = data.knowledgeId;
|
||||
await setFieldsValue({ type: data.type })
|
||||
docType.value = data.type || '';
|
||||
// 新建时:有知识库默认策略则默认选中"使用知识库默认"
|
||||
useKnowledgeDefault.value = knowledgeDefaultSegment.value ? 'default' : 'custom';
|
||||
segmentMetadataRef.value = { segmentStrategy: 'auto', separator: '\\n', maxSegment: 800, overlap: 10 };
|
||||
await setFieldsValue({ type: data.type });
|
||||
}
|
||||
setModalProps({ bodyStyle: { padding: '10px' } });
|
||||
});
|
||||
@@ -67,15 +180,62 @@
|
||||
*/
|
||||
async function handleOk() {
|
||||
try {
|
||||
if (currentStep.value === 0 && knowledgeType.value !== 'memory') {
|
||||
step1Values.value = await validate();
|
||||
currentStep.value = 1;
|
||||
setModalProps({ okText: '保存', minHeight: 400 });
|
||||
if (segmentMetadataRef.value) {
|
||||
await setSegmentFieldsValue({ ...segmentMetadataRef.value });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep.value === 0 && knowledgeType.value === 'memory') {
|
||||
step1Values.value = await validate();
|
||||
}
|
||||
|
||||
setModalProps({ confirmLoading: true });
|
||||
let values = await validate();
|
||||
let values: any = { ...step1Values.value };
|
||||
|
||||
let metadata: any = {};
|
||||
|
||||
//update-begin---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
if (knowledgeType.value === 'memory') {
|
||||
// 记忆库不需要分段策略,metadata 留空
|
||||
} else if (useKnowledgeDefault.value === 'default' && knowledgeDefaultSegment.value) {
|
||||
// 使用知识库默认分段策略,标记即可,后端读取知识库 metadata
|
||||
metadata = { useKnowledgeDefault: true };
|
||||
} else {
|
||||
// update-begin--author:wangshuai--date:2026-04-09--for:【issue/9418】AI知识库上传文件太大向量化失败
|
||||
const segmentFormValues = await validateSegment();
|
||||
metadata = {
|
||||
segmentStrategy: segmentFormValues.segmentStrategy,
|
||||
maxSegment: segmentFormValues.maxSegment,
|
||||
overlap: segmentFormValues.overlap,
|
||||
};
|
||||
if (segmentFormValues.segmentStrategy === 'custom') {
|
||||
metadata = {
|
||||
...metadata,
|
||||
separator: segmentFormValues.separator,
|
||||
customSeparator: segmentFormValues.customSeparator,
|
||||
textRules: segmentFormValues.textRules,
|
||||
};
|
||||
}
|
||||
// update-end--author:wangshuai--date:2026-04-09--for:【issue/9418】AI知识库上传文件太大向量化失败
|
||||
}
|
||||
//update-end---wangshuai---date:20260414 for:【QQYUN-14932】创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略------------
|
||||
if (!unref(isUpdate)) {
|
||||
values.knowledgeId = knowledgeId.value;
|
||||
}
|
||||
if(values.filePath){
|
||||
values.metadata = JSON.stringify({ filePath: values.filePath });
|
||||
metadata.filePath = values.filePath;
|
||||
delete values.filePath;
|
||||
}
|
||||
if(values.website){
|
||||
metadata.website = values.website;
|
||||
delete values.website;
|
||||
}
|
||||
values.metadata = JSON.stringify(metadata);
|
||||
await knowledgeSaveDoc(values);
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
@@ -96,9 +256,16 @@
|
||||
return {
|
||||
registerModal,
|
||||
registerForm,
|
||||
registerSegmentForm,
|
||||
currentStep,
|
||||
title,
|
||||
handleOk,
|
||||
handleCancel,
|
||||
showWebContent,
|
||||
webContentText,
|
||||
knowledgeDefaultSegment,
|
||||
useKnowledgeDefault,
|
||||
knowledgeType,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -108,4 +275,28 @@
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.web-content-preview {
|
||||
margin-top: 8px;
|
||||
.web-content-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
.web-content-body {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<a-card class="model-card" @click="handleEditClick(item)">
|
||||
<div class="model-header">
|
||||
<div class="flex">
|
||||
<img :src="getImage(item.provider)" class="header-img" />
|
||||
<img :src="getImage(item.provider)" :class="['header-img', item.provider === 'VLLM' ? 'header-img-lg' : '']" />
|
||||
<div class="header-text">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,6 +75,9 @@
|
||||
<Icon icon="ant-design:setting-outlined" size="16"></Icon>
|
||||
<span class="ml-4">模型参数配置</span>
|
||||
</a-menu-item>-->
|
||||
<a-menu-item v-if="item.activateFlag" key="deactivate" @click.prevent.stop="handleDeactivateClick(item)">
|
||||
<Icon icon="ant-design:stop-outlined" size="16"></Icon> 取消激活
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete" @click.prevent.stop="handleDeleteClick(item)">
|
||||
<Icon icon="ant-design:delete-outlined" size="16"></Icon> 删除
|
||||
</a-menu-item>
|
||||
@@ -108,7 +111,7 @@
|
||||
import { reactive, ref } from 'vue';
|
||||
import AiModelModal from './components/AiModelModal.vue';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import { deleteModel, list } from './model.api';
|
||||
import { deleteModel, list, editModel } from './model.api';
|
||||
import { imageList } from './model.data';
|
||||
import { Pagination } from 'ant-design-vue';
|
||||
import JInput from '@/components/Form/src/jeecg/components/JInput.vue';
|
||||
@@ -228,6 +231,15 @@
|
||||
await deleteModel({ id: item.id, name: item.name }, reload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消激活模型
|
||||
* @param item
|
||||
*/
|
||||
async function handleDeactivateClick(item) {
|
||||
await editModel({ id: item.id, activateFlag: 0 });
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
@@ -266,6 +278,7 @@
|
||||
handlePageChange,
|
||||
getImage,
|
||||
handleDeleteClick,
|
||||
handleDeactivateClick,
|
||||
searchQuery,
|
||||
searchReset,
|
||||
queryParam,
|
||||
@@ -295,6 +308,11 @@
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 12px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.header-img-lg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.header-text {
|
||||
font-size: 16px;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<a-card class="model-card" @click="handleClick(item)">
|
||||
<div class="model-header">
|
||||
<div class="flex">
|
||||
<img :src="getImage(item.value)" class="header-img" />
|
||||
<img :src="getImage(item.value)" :class="['header-img', item.value === 'VLLM' ? 'header-img-lg' : '']" />
|
||||
<div class="header-text">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,6 +57,21 @@
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<template #extraParams="{ model, field }">
|
||||
<a-input v-model:value="model[field]" readonly placeholder="点击右侧按钮编辑JSON参数">
|
||||
<template #suffix>
|
||||
<FullscreenOutlined style="cursor: pointer;" @click="openExtraParamsModal" />
|
||||
</template>
|
||||
</a-input>
|
||||
<div style="margin-top: 4px; color: #999; font-size: 12px; line-height: 1.5;">
|
||||
目前只支持图片传递,固定格式为 "image_url":"图片地址"(适用于qwen-vl-ocr等视觉模型)<br/>
|
||||
在qwen3.5-plus最新视觉模型中需要额外传递 "incremental_output": true
|
||||
</div>
|
||||
<a-modal v-model:open="extraParamsVisible" title="编辑额外参数" width="600px" @ok="saveExtraParams" destroyOnClose>
|
||||
<JCodeEditor v-model:value="extraParamsTemp" language="javascript" fullScreen height="500px" />
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<template #modelName="{ model, field }">
|
||||
<AutoComplete v-model:value="model[field]" :options="modelNameAddOption" :filter-option="filterOption">
|
||||
<template #option="{ value, label, descr, type }">
|
||||
@@ -122,7 +137,9 @@
|
||||
import { editModel, queryById, saveModel, testConn } from '../model.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
const {createMessage: $message, createConfirm} = useMessage();
|
||||
import { FullscreenOutlined } from '@ant-design/icons-vue';
|
||||
import AiModelSeniorForm from './AiModelSeniorForm.vue';
|
||||
import JCodeEditor from '/@/components/Form/src/jeecg/components/JCodeEditor.vue';
|
||||
import { cloneDeep } from "lodash-es";
|
||||
export default {
|
||||
name: 'AddModelModal',
|
||||
@@ -131,6 +148,8 @@
|
||||
BasicModal,
|
||||
AiModelSeniorForm,
|
||||
AutoComplete,
|
||||
JCodeEditor,
|
||||
FullscreenOutlined,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
@@ -166,6 +185,41 @@
|
||||
const testLoading = ref<boolean>(false);
|
||||
//模型是否已激活
|
||||
const modelActivate = ref<boolean>(false);
|
||||
//特殊参数
|
||||
const extraParamsVisible = ref<boolean>(false);
|
||||
const extraParamsTemp = ref<string>('');
|
||||
|
||||
/**
|
||||
* 打开特殊参数编辑弹窗
|
||||
*/
|
||||
function openExtraParamsModal() {
|
||||
const formVal = getFieldsValue();
|
||||
let val = formVal.extraParams || '';
|
||||
if (val) {
|
||||
try {
|
||||
val = JSON.stringify(JSON.parse(val), null, 2);
|
||||
} catch (e) {}
|
||||
}
|
||||
extraParamsTemp.value = val;
|
||||
extraParamsVisible.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存特殊参数
|
||||
*/
|
||||
function saveExtraParams() {
|
||||
const val = extraParamsTemp.value;
|
||||
if (val) {
|
||||
try {
|
||||
JSON.parse(val);
|
||||
} catch (e) {
|
||||
$message.error('JSON格式不正确,请检查');
|
||||
return;
|
||||
}
|
||||
}
|
||||
setFieldsValue({ extraParams: val });
|
||||
extraParamsVisible.value = false;
|
||||
}
|
||||
|
||||
const getImage = (name) => {
|
||||
return imageList.value[name];
|
||||
@@ -176,11 +230,12 @@
|
||||
}
|
||||
|
||||
//表单配置
|
||||
const [registerForm, { resetFields, setFieldsValue, validate, clearValidate }] = useForm({
|
||||
const [registerForm, { resetFields, setFieldsValue, getFieldsValue, validate, clearValidate }] = useForm({
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
layout: 'vertical',
|
||||
wrapperCol: { span: 24 },
|
||||
labelCol: { span: 24 },
|
||||
});
|
||||
|
||||
//注册modal
|
||||
@@ -204,6 +259,9 @@
|
||||
if(credential.apiKey){
|
||||
values.result.apiKey = credential.apiKey;
|
||||
}
|
||||
if (credential.httpVersionOne) {
|
||||
values.result.httpVersionOne = credential.httpVersionOne;
|
||||
}
|
||||
}
|
||||
let provider = values.result.provider;
|
||||
let data = model.data.filter((item) => {
|
||||
@@ -222,7 +280,12 @@
|
||||
modelActivate.value = false;
|
||||
}
|
||||
if(values.result.modelParams){
|
||||
modelParams.value = JSON.parse(values.result.modelParams)
|
||||
let allParams = JSON.parse(values.result.modelParams);
|
||||
if (allParams.extraParams) {
|
||||
values.result.extraParams = JSON.stringify(allParams.extraParams, null, 2);
|
||||
delete allParams.extraParams;
|
||||
}
|
||||
modelParams.value = allParams;
|
||||
}
|
||||
modelTypeDisabled.value = true;
|
||||
//表单赋值
|
||||
@@ -308,14 +371,25 @@
|
||||
let values = await validate();
|
||||
let credential = {
|
||||
apiKey: values.apiKey,
|
||||
secretKey: values.secretKey
|
||||
secretKey: values.secretKey,
|
||||
httpVersionOne: values.httpVersionOne,
|
||||
}
|
||||
let params = {};
|
||||
if(modelParamsRef.value){
|
||||
let modelParams = modelParamsRef.value.emitChange();
|
||||
if(modelParams){
|
||||
values.modelParams = JSON.stringify(modelParams);
|
||||
let seniorParams = modelParamsRef.value.emitChange();
|
||||
if(seniorParams){
|
||||
params = { ...seniorParams };
|
||||
}
|
||||
}
|
||||
if (values.extraParams) {
|
||||
try {
|
||||
params.extraParams = JSON.parse(values.extraParams);
|
||||
} catch(e) {}
|
||||
}
|
||||
if (Object.keys(params).length > 0) {
|
||||
values.modelParams = JSON.stringify(params);
|
||||
}
|
||||
delete values.extraParams;
|
||||
if(modelActivate.value){
|
||||
values.activateFlag = 1
|
||||
}else{
|
||||
@@ -361,13 +435,26 @@
|
||||
let credential = {
|
||||
apiKey: values.apiKey,
|
||||
secretKey: values.secretKey,
|
||||
httpVersionOne: values.httpVersionOne,
|
||||
};
|
||||
let params = {};
|
||||
if (modelParamsRef.value) {
|
||||
let modelParams = modelParamsRef.value.emitChange();
|
||||
if (modelParams) {
|
||||
values.modelParams = JSON.stringify(modelParams);
|
||||
//update-begin---author:wangshuai---date:2026-03-20---for:【issues/8】保存激活qwen-vl-ocr模型报错---
|
||||
let seniorParams = modelParamsRef.value.emitChange();
|
||||
if (seniorParams) {
|
||||
params = { ...seniorParams };
|
||||
//update-end---author:wangshuai---date:2026-03-20---for:【issues/8】保存激活qwen-vl-ocr模型报错---
|
||||
}
|
||||
}
|
||||
if (values.extraParams) {
|
||||
try {
|
||||
params.extraParams = JSON.parse(values.extraParams);
|
||||
} catch(e) {}
|
||||
}
|
||||
if (Object.keys(params).length > 0) {
|
||||
values.modelParams = JSON.stringify(params);
|
||||
}
|
||||
delete values.extraParams;
|
||||
values.credential = JSON.stringify(credential);
|
||||
if (!values.provider) {
|
||||
values.provider = modelData.value.value;
|
||||
@@ -461,6 +548,10 @@
|
||||
getTitle,
|
||||
test,
|
||||
testLoading,
|
||||
extraParamsVisible,
|
||||
extraParamsTemp,
|
||||
openExtraParamsModal,
|
||||
saveExtraParams,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -492,6 +583,11 @@
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 12px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.header-img-lg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.header-text {
|
||||
width: calc(100% - 80px);
|
||||
|
||||
@@ -21,12 +21,14 @@
|
||||
"title": "DeepSeek",
|
||||
"value": "DEEPSEEK",
|
||||
"LLM": [
|
||||
{"label": "deepseek-reasoner", "value": "deepseek-reasoner","descr": "【官方模型】深度求索 新推出的推理模型R1满血版\n火便全球。\n支持64k上下文,其中支持8k最大回复。","type": "text"},
|
||||
{"label":"deepseek-chat", "value": "deepseek-chat","descr": "最强开源 MoE 模型 DeepSeek-V3,全球首个在代码、数学能力上与GPT-4-Turbo争锋的模型,在代码、数学的多个榜单上位居全球第二;","type": "text"}
|
||||
{"label": "deepseek-v4-pro", "value": "deepseek-v4-pro","descr": "【官方模型】深度求索 新推出的推理模型R1满血版\n火便全球。\n支持64k上下文,其中支持8k最大回复。","type": "text"},
|
||||
{"label": "deepseek-v4-flash", "value": "deepseek-v4-flash","descr": "【官方模型】深度求索 新推出的推理模型R1满血版\n火便全球。\n支持64k上下文,其中支持8k最大回复。","type": "text"},
|
||||
{"label": "deepseek-reasoner", "value": "deepseek-reasoner","descr": " 2026/07/24下线,【官方模型】深度求索 新推出的推理模型R1满血版\n火便全球。\n支持64k上下文,其中支持8k最大回复。","type": "text"},
|
||||
{"label":"deepseek-chat", "value": "deepseek-chat","descr": "2026/07/24下线,最强开源 MoE 模型 DeepSeek-V3,全球首个在代码、数学能力上与GPT-4-Turbo争锋的模型,在代码、数学的多个榜单上位居全球第二;","type": "text"}
|
||||
],
|
||||
"type": ["LLM"],
|
||||
"baseUrl": "https://api.deepseek.com/v1",
|
||||
"LLMDefaultValue": "deepseek-chat"
|
||||
"LLMDefaultValue": "deepseek-v4-pro"
|
||||
},
|
||||
{
|
||||
"title": "Ollama",
|
||||
@@ -181,6 +183,49 @@
|
||||
"LLMDefaultValue": "glm-4-flash",
|
||||
"EMBEDDefaultValue": "Embedding-2",
|
||||
"IMAGEDefaultValue": "CogView-4"
|
||||
},
|
||||
{
|
||||
"title": "Google Gemini",
|
||||
"value": "GOOGLE",
|
||||
"LLM": [
|
||||
{"label": "gemini-2.5-pro", "value": "gemini-2.5-pro","descr": "【Gemini 2.5系列】Google最新旗舰思维模型,具备强大的推理能力。\n\n支持文本和图像输入,文本输出,拥有1M上下文窗口,在编码、数学和科学推理方面表现卓越。","type": "text,image"},
|
||||
{"label": "gemini-2.5-flash", "value": "gemini-2.5-flash","descr": "【Gemini 2.5系列】Google最新的高效思维模型,速度与性能的最佳平衡。\n\n支持文本和图像输入,文本输出,拥有1M上下文窗口,适合高频交互和大规模部署。","type": "text,image"}
|
||||
],
|
||||
"IMAGE": [
|
||||
{"label": "gemini-3-pro-image-preview", "value": "gemini-3-pro-image-preview","descr": "Google最新Gemini 3 Pro图像生成预览模型,具备卓越的图像生成质量。\n\n支持文本到图像生成,在细节表现、风格多样性和文字渲染方面表现出色。","type": "imageGen"},
|
||||
{"label": "gemini-2.5-flash-image", "value": "gemini-2.5-flash-image","descr": "基于Gemini 2.5 Flash的图像生成模型,支持文本到图像生成。\n\n速度快、成本低,适合高频图像生成场景,支持多种风格。","type": "imageGen"}
|
||||
],
|
||||
"type": ["LLM", "IMAGE"],
|
||||
"baseUrl": "https://generativelanguage.googleapis.com/v1beta",
|
||||
"LLMDefaultValue": "gemini-2.5-flash",
|
||||
"IMAGEDefaultValue": "gemini-2.5-flash-image"
|
||||
},
|
||||
{
|
||||
"title": "vLLM",
|
||||
"value": "VLLM",
|
||||
"LLM": [],
|
||||
"EMBED": [],
|
||||
"IMAGE": [],
|
||||
"type": ["LLM", "EMBED", "IMAGE"],
|
||||
"baseUrl": "http://localhost:8000/v1"
|
||||
},
|
||||
{
|
||||
"title": "LM stdio",
|
||||
"value": "LMSTDIO",
|
||||
"LLM": [],
|
||||
"EMBED": [],
|
||||
"IMAGE": [],
|
||||
"type": ["LLM", "EMBED", "IMAGE"],
|
||||
"baseUrl": "http://localhost:1234/v1"
|
||||
},
|
||||
{
|
||||
"title": "Xinference",
|
||||
"value": "XINFERENCE",
|
||||
"LLM": [],
|
||||
"EMBED": [],
|
||||
"IMAGE": [],
|
||||
"type": ["LLM", "EMBED", "IMAGE"],
|
||||
"baseUrl": "http://localhost:9997/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
jeecgboot-vue3/src/views/super/airag/aimodel/icon/gemini.png
Normal file
BIN
jeecgboot-vue3/src/views/super/airag/aimodel/icon/gemini.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
BIN
jeecgboot-vue3/src/views/super/airag/aimodel/icon/imstdio.png
Normal file
BIN
jeecgboot-vue3/src/views/super/airag/aimodel/icon/imstdio.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
jeecgboot-vue3/src/views/super/airag/aimodel/icon/vllm.png
Normal file
BIN
jeecgboot-vue3/src/views/super/airag/aimodel/icon/vllm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg width="173.831879" height="177.308517" viewBox="0 0 173.832 177.309" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<desc>
|
||||
Created with Pixso.
|
||||
</desc>
|
||||
<defs>
|
||||
<linearGradient x1="59.102806" y1="179.252701" x2="146.098419" y2="0.728947" id="paint_linear_0_7_0" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E9A85E"/>
|
||||
<stop offset="1.000000" stop-color="#F52B76"/>
|
||||
</linearGradient>
|
||||
<linearGradient x1="18.536041" y1="179.005981" x2="135.938522" y2="129.504761" id="paint_linear_0_8_0" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E9A85E"/>
|
||||
<stop offset="1.000000" stop-color="#F52B76"/>
|
||||
</linearGradient>
|
||||
<linearGradient x1="0.000004" y1="49.024223" x2="157.555862" y2="121.868401" id="paint_linear_0_9_0" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6A0CF5"/>
|
||||
<stop offset="1.000000" stop-color="#AB66F3"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path id="path" d="M59.1 90.81C64.17 96.44 69.93 101.41 76.23 105.61C81.64 109.24 87.41 112.29 93.45 114.72C104.41 104.34 113.49 92.13 120.29 78.63L159.92 0L90.81 54.46C78.08 64.5 67.32 76.82 59.1 90.81Z" fill="url(#paint_linear_0_7_0)" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||
<path id="path" d="M76.23 105.61C81.64 109.24 87.41 112.29 93.45 114.72C104.41 104.34 113.49 92.13 120.29 78.63L159.92 0L90.81 54.46C78.08 64.5 67.32 76.82 59.1 90.81C64.17 96.44 69.93 101.41 76.23 105.61Z" stroke="#000000" stroke-opacity="0" stroke-width="1.000000"/>
|
||||
<path id="path" d="M53.15 139.95C48.14 136.62 43.42 133.1 38.95 129.5L14.77 177.3L58.23 143.24C56.53 142.15 54.82 141.07 53.15 139.95Z" fill="url(#paint_linear_0_8_0)" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||
<path id="path" d="M38.95 129.5L14.77 177.3L58.23 143.24C56.53 142.15 54.82 141.07 53.15 139.95C48.14 136.62 43.42 133.1 38.95 129.5Z" stroke="#000000" stroke-opacity="0" stroke-width="1.000000"/>
|
||||
<path id="path" d="M141.86 63.63C154.87 80.88 158.67 100.04 149.85 113.23C136.97 132.49 102.06 131.75 71.87 111.58C41.68 91.41 27.65 59.45 40.53 40.19C49.34 27 68.5 23.19 89.42 28.59C53.25 13.23 18.83 14.75 5.48 34.67C-11.27 59.75 11.61 104.48 56.58 134.48C101.54 164.48 151.59 168.55 168.34 143.49C181.68 123.53 169.88 91.16 141.86 63.63Z" fill="url(#paint_linear_0_9_0)" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||
<path id="path" d="M149.85 113.23C136.97 132.49 102.06 131.75 71.87 111.58C41.68 91.41 27.65 59.45 40.53 40.19C49.34 27 68.5 23.19 89.42 28.59C53.25 13.23 18.83 14.75 5.48 34.67C-11.27 59.75 11.61 104.48 56.58 134.48C101.54 164.48 151.59 168.55 168.34 143.49C181.68 123.53 169.88 91.16 141.86 63.63C154.87 80.88 158.67 100.04 149.85 113.23Z" stroke="#000000" stroke-opacity="0" stroke-width="1.000000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -7,6 +7,10 @@ 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 xinference from './icon/xinference.svg';
|
||||
import vllm from './icon/vllm.png';
|
||||
import imstdio from './icon/imstdio.png';
|
||||
import gemini from './icon/gemini.png';
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
@@ -48,7 +52,7 @@ export const formSchema: FormSchema[] = [
|
||||
{
|
||||
label: 'API Key',
|
||||
field: 'apiKey',
|
||||
required: true,
|
||||
required: ({ values }) => values.provider !== 'XINFERENCE',
|
||||
component: 'InputPassword',
|
||||
componentProps: {
|
||||
autocomplete: 'new-password',
|
||||
@@ -67,12 +71,35 @@ 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==="ANTHROPIC"){
|
||||
|| values.provider==="ZHIPU" || values.provider==="QWEN" || values.provider==="ANTHROPIC"
|
||||
|| values.provider==="XINFERENCE" || values.provider==="VLLM" || values.provider === 'LMSTDIO'
|
||||
|| values.provider === "GOOGLE"){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'HTTP1.1协议',
|
||||
field: 'httpVersionOne',
|
||||
component: 'Switch',
|
||||
defaultValue: 1,
|
||||
helpMessage: '是否使用HTTP1.1协议,在长时间无响应的情况下,可以尝试关闭此开关',
|
||||
componentProps: {
|
||||
checkedValue: 1,
|
||||
unCheckedValue: 0,
|
||||
},
|
||||
ifShow: ({ values }) => {
|
||||
return values.provider === 'VLLM' || values.provider === 'LMSTDIO' || values.provider === 'XINFERENCE';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '额外参数',
|
||||
field: 'extraParams',
|
||||
slot: 'extraParams',
|
||||
component: 'Input',
|
||||
ifShow: ({ values }) => values.modelType === 'LLM',
|
||||
},
|
||||
{
|
||||
label: '供应者',
|
||||
field: 'provider',
|
||||
@@ -94,4 +121,8 @@ export const imageList = ref<any>({
|
||||
QIANFAN: qianfan,
|
||||
QWEN: qianwen,
|
||||
ZHIPU: zhipuai,
|
||||
XINFERENCE: xinference,
|
||||
VLLM: vllm,
|
||||
LMSTDIO: imstdio,
|
||||
GOOGLE: gemini,
|
||||
});
|
||||
|
||||
414
jeecgboot-vue3/src/views/super/airag/aiposter/AiPainting.vue
Normal file
414
jeecgboot-vue3/src/views/super/airag/aiposter/AiPainting.vue
Normal file
@@ -0,0 +1,414 @@
|
||||
<template>
|
||||
<div class="content-wrapper">
|
||||
<!-- 中间参数配置 -->
|
||||
<div class="config-panel">
|
||||
<div class="config-tabs">
|
||||
<a-tabs v-model:activeKey="configTab" :tabBarStyle="{ margin: 0 }">
|
||||
<a-tab-pane key="draw" tab="绘图" />
|
||||
<a-tab-pane key="face" tab="换脸" />
|
||||
<a-tab-pane key="mix" tab="混图" />
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 示例按钮区域 -->
|
||||
<div class="example-buttons" v-if="configTab === 'mix'">
|
||||
<a-tooltip title="工作证制作">
|
||||
<a-button class="example-btn" size="small" @click="applyExample('work_card')">示例一</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="换衣">
|
||||
<a-button class="example-btn" size="small" @click="applyExample('change_clothes')">示例二</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-container">
|
||||
<BasicForm @register="registerForm" />
|
||||
|
||||
<div class="instructions" v-if="configTab === 'face'">
|
||||
<div class="title">说明:</div>
|
||||
<p>1 图片都必须包含脸,否则出不来图</p>
|
||||
<p>2 "明星图"可以先用mj绘画制作出来</p>
|
||||
<p>3 "明星图"其实动漫图也行</p>
|
||||
<p>4 "你的头像"建议用一寸个人照</p>
|
||||
</div>
|
||||
|
||||
<div class="instructions" v-if="configTab === 'mix'">
|
||||
<div class="title">说明:</div>
|
||||
<p>1 合成至少2张图片</p>
|
||||
<p>2 最多可传3张图</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-container">
|
||||
<a-button type="primary" size="large" block @click="handleGenerate" :loading="loading">
|
||||
<Icon icon="ant-design:thunderbolt-outlined" />
|
||||
立即生成
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧图片生成结果 -->
|
||||
<div class="preview-panel">
|
||||
<div class="panel-title">生成结果</div>
|
||||
<div class="preview-content">
|
||||
<div v-if="!generatedImage && !loading" class="empty-state">
|
||||
<Icon icon="ant-design:picture-outlined" size="64" color="#ccc" />
|
||||
<p>在左侧配置参数并点击生成</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !generatedImage" class="loading-state">
|
||||
<a-spin size="large" tip="正在绘制图片,请稍候..." />
|
||||
</div>
|
||||
|
||||
<div v-if="generatedImage" class="result-image-wrapper group">
|
||||
<img :src="generatedImage" class="result-image" alt="Generated Image" />
|
||||
<div class="image-actions">
|
||||
<a-button type="primary" ghost @click="handlePreview">
|
||||
<Icon icon="ant-design:eye-outlined" />
|
||||
预览
|
||||
</a-button>
|
||||
<a-button type="primary" ghost @click="handleDownload">
|
||||
<Icon icon="ant-design:download-outlined" />
|
||||
下载
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ImageViewer v-if="previewVisible" :imageUrl="generatedImage" @hide="previewVisible = false" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { BasicForm, useForm } from '@/components/Form';
|
||||
import { drawFormSchema, faceSwapFormSchema, mixFormSchema } from './AiPoster.data';
|
||||
import ImageViewer from '../aiapp/chat/components/ImageViewer.vue';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { Icon } from '@/components/Icon';
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const loading = ref(false);
|
||||
//update-begin---author:wangshuai---date:2026-04-15---for:【QQYUN-14944】AI绘画改为异步轮询,支持切换菜单后继续查询
|
||||
const PAINTING_TASK_ID_KEY = 'ai_painting_task_id';
|
||||
let pollTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
//update-end---author:wangshuai---date:2026-04-15---for:【QQYUN-14944】AI绘画改为异步轮询,支持切换菜单后继续查询
|
||||
const generatedImage = ref('');
|
||||
const previewVisible = ref(false);
|
||||
const configTab = ref('draw');
|
||||
|
||||
const { domainUrl } = useGlobSetting();
|
||||
const [registerForm, { validate, resetSchema, setFieldsValue }] = useForm({
|
||||
schemas: drawFormSchema,
|
||||
labelWidth: 100,
|
||||
actionColOptions: { span: 24 },
|
||||
showActionButtonGroup: false,
|
||||
});
|
||||
|
||||
watch(configTab, (val) => {
|
||||
if (val === 'draw') {
|
||||
resetSchema(drawFormSchema);
|
||||
} else if (val === 'face') {
|
||||
resetSchema(faceSwapFormSchema);
|
||||
} else if (val === 'mix') {
|
||||
resetSchema(mixFormSchema);
|
||||
} else {
|
||||
// Default to draw or empty for mix for now
|
||||
resetSchema(drawFormSchema);
|
||||
}
|
||||
});
|
||||
|
||||
//update-begin---author:wangshuai---date:2026-04-15---for:【QQYUN-14944】AI绘画改为异步轮询,支持切换菜单后继续查询
|
||||
/** 轮询查询任务结果 */
|
||||
function startPolling(taskId: string) {
|
||||
const poll = () => {
|
||||
defHttp
|
||||
.get({ url: `/airag/chat/getAiPosterResult/${taskId}` }, { isTransformResponse: false })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
if (res.result === 'pending' || res.result === null) {
|
||||
pollTimer = setTimeout(poll, 3000);
|
||||
} else {
|
||||
let imageUrl = res.result as string;
|
||||
const reg = /#\s*{\s*domainURL\s*}/g;
|
||||
imageUrl = imageUrl.replace(reg, domainUrl + '/sys/common/static');
|
||||
generatedImage.value = imageUrl;
|
||||
loading.value = false;
|
||||
localStorage.removeItem(PAINTING_TASK_ID_KEY);
|
||||
createMessage.success('图片生成成功!');
|
||||
}
|
||||
} else {
|
||||
loading.value = false;
|
||||
localStorage.removeItem(PAINTING_TASK_ID_KEY);
|
||||
createMessage.warning(res.message || '图片生成失败!');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
pollTimer = setTimeout(poll, 3000);
|
||||
});
|
||||
};
|
||||
poll();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const savedTaskId = localStorage.getItem(PAINTING_TASK_ID_KEY);
|
||||
if (savedTaskId) {
|
||||
loading.value = true;
|
||||
generatedImage.value = '';
|
||||
startPolling(savedTaskId);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollTimer) {
|
||||
clearTimeout(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
});
|
||||
//update-end---author:wangshuai---date:2026-04-15---for:【QQYUN-14944】AI绘画改为异步轮询,支持切换菜单后继续查询
|
||||
|
||||
async function handleGenerate() {
|
||||
try {
|
||||
const values = await validate();
|
||||
loading.value = true;
|
||||
generatedImage.value = '';
|
||||
values.type = configTab.value;
|
||||
|
||||
if (configTab.value === 'face') {
|
||||
if (values.sourceImage && values.targetImage) {
|
||||
values.imageUrl = values.sourceImage + ',' + values.targetImage;
|
||||
delete values.sourceImage;
|
||||
delete values.targetImage;
|
||||
}
|
||||
values.content = '将图1的面部特征替换到图2的面部区域,保留图1五官细节,保持图2身体姿态,面部融合自然,高分辨率,写实风格';
|
||||
}
|
||||
|
||||
//update-begin---author:wangshuai---date:2026-04-15---for:【QQYUN-14944】改为异步提交,获取taskId后开始轮询
|
||||
const res = await defHttp.post(
|
||||
{ url: '/airag/chat/genAiPosterAsync', params: values },
|
||||
{ isTransformResponse: false },
|
||||
);
|
||||
if (res.success && res.result) {
|
||||
const taskId = res.result as string;
|
||||
localStorage.setItem(PAINTING_TASK_ID_KEY, taskId);
|
||||
startPolling(taskId);
|
||||
} else {
|
||||
loading.value = false;
|
||||
createMessage.warning('提交任务失败!');
|
||||
}
|
||||
//update-end---author:wangshuai---date:2026-04-15---for:【QQYUN-14944】改为异步提交,获取taskId后开始轮询
|
||||
} catch (error) {
|
||||
console.error('Validation failed:', error);
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePreview() {
|
||||
previewVisible.value = true;
|
||||
}
|
||||
|
||||
function applyExample(type: string) {
|
||||
if (type === 'work_card') {
|
||||
setFieldsValue({
|
||||
imageUrl:
|
||||
'https://jeecgdev.oss-cn-beijing.aliyuncs.com/upload/test/afdad9ea-077f-44a4-9d85-c26cde7aceed_1770703400282.png,https://jeecgdev.oss-cn-beijing.aliyuncs.com/upload/test/4e4d1886-fb3b-4c01-abf6-25e546a1253e_1770703403479.png',
|
||||
content:
|
||||
'以[图1]名片设计稿的构图与磨砂玻璃质感为模板,为[图2]人物生成竖版工作卡。圆角半透明卡面,柔和高光与浅投影;人物胸像置于中上区域;左下排版姓名/职位/公司/电话,极简无衬线字体,留白均衡。右上角放置[图1]人物的可爱3D卡通形象,打破边界半浮出卡片并投下轻影,形成层次与视觉焦点。整体明亮自然光、真实材质细节,不添加多余图案与元素。人物需要清晰,不要模糊。',
|
||||
});
|
||||
} else if (type === 'change_clothes') {
|
||||
setFieldsValue({
|
||||
imageUrl:
|
||||
'https://jeecgdev.oss-cn-beijing.aliyuncs.com/upload/test/4e4d1886-fb3b-4c01-abf6-25e546a1253e_1770703403479.png,https://jeecgdev.oss-cn-beijing.aliyuncs.com/upload/test/63706787-4072-4cba-ad88-385e0584b020_1770703456766.png',
|
||||
content: '将【图一】的衣服换到【图二】中。',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片导出
|
||||
*/
|
||||
function handleDownload() {
|
||||
if (!generatedImage.value) {
|
||||
return;
|
||||
}
|
||||
const a = document.createElement('a');
|
||||
a.href = generatedImage.value;
|
||||
a.download = `ai-painting-${Date.now()}.jpg`;
|
||||
a.target = '_blank';
|
||||
a.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
height: 100%; /* Ensure it takes full height of parent */
|
||||
}
|
||||
|
||||
.config-panel {
|
||||
width: 550px;
|
||||
min-width: 350px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.config-tabs {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.ant-tabs-nav::before) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-tab) {
|
||||
padding: 8px 0;
|
||||
margin: 0 32px 0 0;
|
||||
font-size: 16px;
|
||||
|
||||
&.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: #00b96b;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-ink-bar) {
|
||||
background: #00b96b;
|
||||
}
|
||||
}
|
||||
|
||||
.example-buttons {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
z-index: 10;
|
||||
|
||||
.example-btn {
|
||||
color: #666;
|
||||
border-color: #d9d9d9;
|
||||
|
||||
&:hover {
|
||||
color: #00b96b;
|
||||
border-color: #00b96b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.instructions {
|
||||
margin-top: 20px;
|
||||
padding: 0 10px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-container {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
background: #f7f8fc;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
margin-bottom: 20px;
|
||||
padding-left: 8px;
|
||||
border-left: 4px solid #1890ff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #8f959e;
|
||||
|
||||
p {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.result-image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.result-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.image-actions {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
border-radius: 8px;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
&:hover .image-actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -40,7 +40,264 @@ export const formSchema: FormSchema[] = [
|
||||
field: 'imageSize',
|
||||
label: '图片尺寸',
|
||||
component: 'Select',
|
||||
defaultValue: '1024*1024',
|
||||
defaultValue: '720*1280',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '1:1 (1024x1024)', value: '1024*1024' },
|
||||
{ label: '16:9 (1280x720)', value: '1280*720' },
|
||||
{ label: '9:16 (720x1280)', value: '720*1280' },
|
||||
{ label: '4:3 (1024x768)', value: '1024*768' },
|
||||
{ label: '3:4 (768x1024)', value: '768*1024' },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 混图表单
|
||||
*/
|
||||
export const mixFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'drawModelId',
|
||||
label: '模型',
|
||||
component: 'JDictSelectTag',
|
||||
required: true,
|
||||
helpMessage: [
|
||||
'1、需要选择在模型中已有的图像模型',
|
||||
'2、当前支持通义万象模型wan2.5-i2i-preview',
|
||||
],
|
||||
componentProps: {
|
||||
dictCode: "airag_model where model_type = 'IMAGE' and activate_flag = 1,name,id",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'imageSize',
|
||||
label: '尺寸',
|
||||
component: 'Select',
|
||||
defaultValue: '720*1280',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '1:1 (1024x1024)', value: '1024*1024' },
|
||||
{ label: '16:9 (1280x720)', value: '1280*720' },
|
||||
{ label: '9:16 (720x1280)', value: '720*1280' },
|
||||
{ label: '4:3 (1024x768)', value: '1024*768' },
|
||||
{ label: '3:4 (768x1024)', value: '768*1024' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'imageUrl',
|
||||
label: '上传图像',
|
||||
component: 'JImageUpload',
|
||||
required: true,
|
||||
componentProps: {
|
||||
fileMax: 3,
|
||||
text: '上传图像',
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
validator: async (_, value) => {
|
||||
if (!value) {
|
||||
return Promise.reject('请上传图像');
|
||||
}
|
||||
const images = value.split(',');
|
||||
if (images.length < 2) {
|
||||
return Promise.reject('合成至少2张图片');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
label: '提示词',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
rows: 4,
|
||||
placeholder: '如将图一的话花瓶放到图二中',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 绘画的表单
|
||||
*/
|
||||
export const drawFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'drawModelId',
|
||||
label: '模型',
|
||||
component: 'JDictSelectTag',
|
||||
required: true,
|
||||
helpMessage: [
|
||||
'1、需要选择在模型中已有的图像模型',
|
||||
'2、智普语言模型不支持尺寸设置',
|
||||
"3、openAi旧版模型如(dall-e-2)需要选择尺寸,新版模型直接输入'竖版: 9:16即可'",
|
||||
'4、当前只有千问万象模型(wanx2.1-imageedit,wan2.5-i2i-preview)支持图生图',
|
||||
'5、wan2.5-i2i-preview支持多张图片',
|
||||
'6、当前文生图openAi效果最佳',
|
||||
],
|
||||
componentProps: {
|
||||
dictCode: "airag_model where model_type = 'IMAGE' and activate_flag = 1,name,id",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
label: '提示词',
|
||||
component: 'InputTextArea',
|
||||
required: true,
|
||||
componentProps: {
|
||||
rows: 5,
|
||||
placeholder: '请输入提示词,例如:一只可爱的猫咪',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'imageSize',
|
||||
label: '图片尺寸',
|
||||
component: 'Select',
|
||||
defaultValue: '720*1280',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '1:1 (1024x1024)', value: '1024*1024' },
|
||||
{ label: '16:9 (1280x720)', value: '1280*720' },
|
||||
{ label: '9:16 (720x1280)', value: '720*1280' },
|
||||
{ label: '4:3 (1024x768)', value: '1024*768' },
|
||||
{ label: '3:4 (768x1024)', value: '768*1024' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'style',
|
||||
label: '风格',
|
||||
component: 'Select',
|
||||
defaultValue: 'modernOrganic',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '赛博朋克', value: 'cyberpunk' },
|
||||
{ label: '星际', value: 'star' },
|
||||
{ label: '动漫', value: 'anime' },
|
||||
{ label: '日本漫画', value: 'japaneseComicsManga' },
|
||||
{ label: '水墨画风格', value: 'inkWashPaintingStyle' },
|
||||
{ label: '原创', value: 'original' },
|
||||
{ label: '风景画', value: 'landscape' },
|
||||
{ label: '插画', value: 'illustration' },
|
||||
{ label: '漫画', value: 'manga' },
|
||||
{ label: '现代自然', value: 'modernOrganic' },
|
||||
{ label: '创世纪', value: 'genesis' },
|
||||
{ label: '海报风格', value: 'posterstyle' },
|
||||
{ label: '超现实主义', value: 'surrealism' },
|
||||
{ label: '素描', value: 'sketch' },
|
||||
{ label: '写实', value: 'realism' },
|
||||
{ label: '水彩画', value: 'watercolorPainting' },
|
||||
{ label: '立体主义', value: 'cubism' },
|
||||
{ label: '黑白', value: 'blackAndWhite' },
|
||||
{ label: '胶片摄影风格', value: 'fmPhotography' },
|
||||
{ label: '电影化', value: 'cinematic' },
|
||||
{ label: '清晰的面部特征', value: 'clearFacialFeatures' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'visualAngle',
|
||||
label: '视角',
|
||||
component: 'Select',
|
||||
defaultValue: 'frontView',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '宽视角', value: 'wideView' },
|
||||
{ label: '鸟瞰视角', value: 'birdView' },
|
||||
{ label: '顶视角', value: 'topView' },
|
||||
{ label: '仰视角', value: 'upview' },
|
||||
{ label: '正面视角', value: 'frontView' },
|
||||
{ label: '头部特写', value: 'headshot' },
|
||||
{ label: '超广角视角', value: 'ultrawideshot' },
|
||||
{ label: '中景', value: 'mediumShot' },
|
||||
{ label: '远景', value: 'longShot' },
|
||||
{ label: '景深', value: 'depthOfField' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'characterShot',
|
||||
label: '人物镜头',
|
||||
component: 'Select',
|
||||
defaultValue: 'fullLengthShot',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '脸部特写', value: 'faceShot' },
|
||||
{ label: '大特写', value: 'bigCloseUp' },
|
||||
{ label: '特写', value: 'closeUp' },
|
||||
{ label: '腰部以上', value: 'waistShot' },
|
||||
{ label: '膝盖以上', value: 'kneeShot' },
|
||||
{ label: '全身照', value: 'fullLengthShot' },
|
||||
{ label: '极远景', value: 'extraLongShot' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'lighting',
|
||||
label: '灯光',
|
||||
component: 'Select',
|
||||
defaultValue: 'naturalLight',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '冷光', value: 'coldLight' },
|
||||
{ label: '暖光', value: 'warmLight' },
|
||||
{ label: '硬光', value: 'hardLighting' },
|
||||
{ label: '戏剧性光线', value: 'dramaticLight' },
|
||||
{ label: '反射光', value: 'reflectionLight' },
|
||||
{ label: '薄雾', value: 'mistyFoggy' },
|
||||
{ label: '自然光', value: 'naturalLight' },
|
||||
{ label: '阳光', value: 'sunLight' },
|
||||
{ label: '情绪化', value: 'moody' },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 换脸表单
|
||||
*/
|
||||
export const faceSwapFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'drawModelId',
|
||||
label: '模型',
|
||||
component: 'JDictSelectTag',
|
||||
required: true,
|
||||
helpMessage: [
|
||||
'1、需要选择在模型中已有的图像模型',
|
||||
'2、当前只支持通义万象模型(wan2.5-i2i-preview)'
|
||||
],
|
||||
componentProps: {
|
||||
dictCode: "airag_model where model_type = 'IMAGE' and activate_flag = 1,name,id",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sourceImage',
|
||||
label: '你的头像',
|
||||
component: 'JImageUpload',
|
||||
required: true,
|
||||
componentProps: {
|
||||
fileMax: 1,
|
||||
text: '上传头像',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'targetImage',
|
||||
label: '明星图',
|
||||
component: 'JImageUpload',
|
||||
required: true,
|
||||
componentProps: {
|
||||
fileMax: 1,
|
||||
text: '上传明星图',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'imageSize',
|
||||
label: '图片尺寸',
|
||||
component: 'Select',
|
||||
defaultValue: '720*1280',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '1:1 (1024x1024)', value: '1024*1024' },
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<div class="ai-poster-page">
|
||||
<div class="page-header">
|
||||
<span class="title">AI 海报生成</span>
|
||||
<span class="subtitle">输入提示词,快速生成精美海报</span>
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="poster" tab="AI 海报" />
|
||||
<a-tab-pane key="painting" tab="AI 绘画" />
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content-wrapper" v-show="activeKey === 'poster'">
|
||||
<!-- 左侧模板选择 -->
|
||||
<div class="template-panel">
|
||||
<div class="panel-title">模板选择</div>
|
||||
@@ -47,7 +49,7 @@
|
||||
<p>在左侧配置参数并点击生成</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div v-if="loading && !generatedImage" class="loading-state">
|
||||
<a-spin size="large" tip="正在绘制海报,请稍候..." />
|
||||
</div>
|
||||
|
||||
@@ -68,12 +70,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AiPainting v-if="activeKey === 'painting'" />
|
||||
|
||||
<ImageViewer v-if="previewVisible" :imageUrl="generatedImage" @hide="previewVisible = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { BasicForm, useForm } from '@/components/Form';
|
||||
import { formSchema } from './AiPoster.data';
|
||||
import ImageViewer from '../aiapp/chat/components/ImageViewer.vue';
|
||||
@@ -81,6 +85,12 @@
|
||||
import { Icon } from '@/components/Icon';
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
import AiPainting from './AiPainting.vue';
|
||||
|
||||
const TASK_ID_KEY = 'ai_poster_task_id';
|
||||
const ACTIVE_TAB_KEY = 'ai_poster_active_tab';
|
||||
const activeKey = ref(localStorage.getItem(ACTIVE_TAB_KEY) || 'poster');
|
||||
watch(activeKey, (val) => localStorage.setItem(ACTIVE_TAB_KEY, val));
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const loading = ref(false);
|
||||
@@ -88,6 +98,7 @@
|
||||
const previewVisible = ref(false);
|
||||
|
||||
const activeTemplateId = ref<number | null>(null);
|
||||
let pollTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const templates = [
|
||||
{
|
||||
@@ -96,14 +107,14 @@
|
||||
prompt:
|
||||
'淡雅政务风横版海报,主色调浅蓝 + 米白 + 淡灰,扁平化矢量风格,叠加细腻宣纸纹理;画面核心元素:简约政务办公楼轮廓(线条简洁)、金色钢笔、展开的公文册、淡蓝色祥云纹样、橄榄枝装饰;背景是米白渐变 + 浅蓝竖条肌理,点缀细金色边框;文字设计:居中用黑体写‘政务为民・高效规范’,下方配‘用心服务・务实笃行’浅蓝小字;整体氛围淡雅庄重、专业简洁,层次分明,光影柔和,高清细节,竖版 9:16 构图',
|
||||
size: '720*1280',
|
||||
url: 'https://minio.jeecg.com/otatest/simple_1767767784521.png',
|
||||
url: 'https://upload.jeecg.com/jeecg/AI/simple.png',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '节日海报',
|
||||
prompt:
|
||||
'国潮中国风春节竖版海报,主色调红金 + 暖橙渐变,国潮插画风格,矢量扁平 + 柔和渐变质感,叠加细腻宣纸纹理;画面层次:前景是红色剪纸风梅花、金色福字贴纸、饱满水饺、红色灯笼串,中景是红墙金瓦的传统民居屋檐、飘带式祥云,远景是淡金色烟花绽放 + 暖红色光晕背景;点缀金色铜钱纹、折纸兔子、如意纹样;画面中央偏上用金色书法字体写‘新春大吉’,下方配\'万事如意\'四字楷书;整体氛围喜庆祥和、团圆温馨,层次分明主次清晰,光影柔和不刺眼,高清细节,竖版 9:16 构图',
|
||||
url: 'https://minio.jeecg.com/otatest/image89444392111_1767844276342.png',
|
||||
url: 'https://upload.jeecg.com/jeecg/AI/image89444392111.png',
|
||||
size: '720*1280',
|
||||
},
|
||||
{
|
||||
@@ -112,7 +123,7 @@
|
||||
prompt:
|
||||
'未来科技感宣传海报,主色调蓝紫渐变 + 银白金属色,冷光霓虹光效,赛博朋克线条质感;画面核心元素:全息投影的地球数据模型、流动的蓝色数据流、发光的电路板纹理、悬浮的芯片与机械齿轮、未来感建筑轮廓;点缀粒子光效、透明全息界面、霓虹光带;文字设计:居中用未来感无衬线字体写‘科技赋能・智启新程’,下方配‘创新驱动・引领未来’小字,字体带轻微发光描边;整体氛围简洁高级、充满未来感,层次分明,光影锐利,高清细节,横版 16:9 构图',
|
||||
size: '720*1280',
|
||||
url: 'https://minio.jeecg.com/otatest/technology_1767765484936.png',
|
||||
url: 'https://upload.jeecg.com/jeecg/AI/technology.png',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
@@ -120,7 +131,7 @@
|
||||
prompt:
|
||||
'民国风优雅复古竖版海报,主色调米黄 + 豆沙红 + 墨黑,低饱和度胶片质感,叠加老报纸纹理与轻微颗粒感;画面核心元素:穿月白旗袍的女性侧影(盘发配珍珠发簪)、油纸伞、复古留声机、雕花木质窗棂、缠绕珍珠的藤蔓花纹;背景是模糊的老上海石库门建筑轮廓,点缀淡粉色玉兰花、复古字体排版的诗句(‘岁月静好,温婉如初’);文字设计:上方用民国手写体写‘雅致时光’,下方配衬线字体‘复刻民国风雅’,字体带轻微做旧效果;整体氛围温婉知性、静谧典雅,光影柔和(侧光勾勒人物轮廓),层次分明,高清细节,竖版 9:16 构图',
|
||||
size: '720*1280',
|
||||
url: 'https://minio.jeecg.com/otatest/retro_1767765748402.png',
|
||||
url: 'https://upload.jeecg.com/jeecg/AI/retro.png',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
@@ -128,7 +139,7 @@
|
||||
prompt:
|
||||
'国潮赛博朋克横版海报,主色调中国红 + 深空黑 + 鎏金霓虹,传统纹样与科技元素碰撞,叠加红金渐变光效 + 竹简纹理;画面核心元素:龙形霓虹光带(龙身缠绕电路板)、红墙金瓦的赛博风古建筑(屋檐挂霓虹灯笼)、穿汉服改良款的赛博人物(配发光发簪 / 机械袖)、全息投影的汉字霓虹灯牌(‘江湖’‘未来’);点缀祥云数据流、金属质感的传统回纹、悬浮的鎏金元宝状机械装置;文字设计:上方用金色书法字体写‘赛博江湖’,下方配‘TECH & TRADITION’英文,字体带红金霓虹发光效果;整体氛围大气炫酷、传统与未来交融,光影强烈且富有冲击力,层次分明,高清细节,横版 16:9 构图',
|
||||
size: '720*1280',
|
||||
url: 'https://minio.jeecg.com/otatest/cyberpunk_1767766076979.png',
|
||||
url: 'https://upload.jeecg.com/jeecg/AI/cyberpunk.png',
|
||||
},
|
||||
];
|
||||
const { domainUrl } = useGlobSetting();
|
||||
@@ -153,35 +164,87 @@
|
||||
createMessage.success(`已应用模板:${template.name}`);
|
||||
}
|
||||
|
||||
//update-begin---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
/** 轮询查询任务结果 */
|
||||
function startPolling(taskId: string) {
|
||||
const poll = () => {
|
||||
defHttp
|
||||
.get({ url: `/airag/chat/getAiPosterResult/${taskId}` }, { isTransformResponse: false })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
if (res.result === 'pending' || res.result === null) {
|
||||
// 继续轮询
|
||||
pollTimer = setTimeout(poll, 3000);
|
||||
} else {
|
||||
// 成功
|
||||
let imageUrl = res.result as string;
|
||||
const reg = /#\s*{\s*domainURL\s*}/g;
|
||||
imageUrl = imageUrl.replace(reg, domainUrl + '/sys/common/static');
|
||||
generatedImage.value = imageUrl;
|
||||
loading.value = false;
|
||||
localStorage.removeItem(TASK_ID_KEY);
|
||||
createMessage.success('海报生成成功!');
|
||||
}
|
||||
} else {
|
||||
// 失败
|
||||
loading.value = false;
|
||||
localStorage.removeItem(TASK_ID_KEY);
|
||||
createMessage.warning(res.message || '海报生成失败!');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
pollTimer = setTimeout(poll, 3000);
|
||||
});
|
||||
};
|
||||
poll();
|
||||
}
|
||||
//update-end---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
|
||||
async function handleGenerate() {
|
||||
try {
|
||||
const values = await validate();
|
||||
console.log('Generating with values:', values);
|
||||
loading.value = true;
|
||||
generatedImage.value = '';
|
||||
|
||||
setTimeout(() => {
|
||||
defHttp
|
||||
.post({ url: '/airag/chat/genAiPoster', params: values, timeout: 5 * 60 * 1000 }, { isTransformResponse: false })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
let reg = /#\s*{\s*domainURL\s*}/g;
|
||||
res.result = res.result.replace(reg, domainUrl + '/sys/common/static');
|
||||
generatedImage.value = res.result;
|
||||
createMessage.success('海报生成成功!');
|
||||
} else {
|
||||
createMessage.warning('海报生成失败!');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}, 2000);
|
||||
//update-begin---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
values.type = 'poster';
|
||||
const res = await defHttp.post(
|
||||
{ url: '/airag/chat/genAiPosterAsync', params: values },
|
||||
{ isTransformResponse: false },
|
||||
);
|
||||
if (res.success && res.result) {
|
||||
const taskId = res.result as string;
|
||||
localStorage.setItem(TASK_ID_KEY, taskId);
|
||||
startPolling(taskId);
|
||||
} else {
|
||||
loading.value = false;
|
||||
createMessage.warning('提交任务失败!');
|
||||
}
|
||||
//update-end---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
} catch (error) {
|
||||
loading.value = false;
|
||||
console.error('Validation failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
onMounted(() => {
|
||||
// 切换回菜单时,若有未完成的任务则恢复轮询
|
||||
const savedTaskId = localStorage.getItem(TASK_ID_KEY);
|
||||
if (savedTaskId) {
|
||||
loading.value = true;
|
||||
generatedImage.value = '';
|
||||
startPolling(savedTaskId);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollTimer) {
|
||||
clearTimeout(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
});
|
||||
//update-end---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
|
||||
function handlePreview() {
|
||||
previewVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const { createConfirm } = useMessage();
|
||||
|
||||
enum Api {
|
||||
list = '/airag/prompts/list',
|
||||
queryById = '/airag/prompts/queryById',
|
||||
save='/airag/prompts/add',
|
||||
edit='/airag/prompts/edit',
|
||||
deleteOne = '/airag/prompts/delete',
|
||||
@@ -30,6 +31,13 @@ export const getImportUrl = Api.importExcel;
|
||||
export const list = (params) =>
|
||||
defHttp.get({url: Api.list, params});
|
||||
|
||||
/**
|
||||
* 根据ID查询提示词详情
|
||||
* @param id 提示词ID
|
||||
*/
|
||||
export const queryById = (id: string) =>
|
||||
defHttp.get({url: Api.queryById, params: {id}});
|
||||
|
||||
/**
|
||||
* 删除单个
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BasicColumn } from '/@/components/Table';
|
||||
import { FormSchema } from '/@/components/Table';
|
||||
import {duplicateCheckDelay} from "@/views/system/user/user.api";
|
||||
export const DESFORM_NAME_MAX_LENGTH = 40;
|
||||
import {pinyin} from "pinyin-pro";
|
||||
//列表数据
|
||||
export const columns: BasicColumn[] = [
|
||||
@@ -62,7 +61,7 @@ export const formSchema: FormSchema[] = [
|
||||
componentProps: ({ formModel }) => {
|
||||
return {
|
||||
placeholder: '例如:SQL转换',
|
||||
maxlength: DESFORM_NAME_MAX_LENGTH,
|
||||
maxlength: 40,
|
||||
showCount: true,
|
||||
onChange: (e: ChangeEvent) => {
|
||||
if(formModel.id){
|
||||
|
||||
37
jeecgboot-vue3/src/views/super/airag/aivideo/AiVideo.api.ts
Normal file
37
jeecgboot-vue3/src/views/super/airag/aivideo/AiVideo.api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// AI Video API 接口配置
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
|
||||
enum Api {
|
||||
submit = '/airag/video/submit',
|
||||
query = '/airag/video/query',
|
||||
listByUser = '/airag/video/listByUser',
|
||||
deleteRecord = '/airag/video/deleteVideoRecord',
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交视频生成任务
|
||||
*/
|
||||
export const submitVideoTask = (params: any) => {
|
||||
return defHttp.post({ url: Api.submit, params }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询视频生成任务状态
|
||||
*/
|
||||
export const queryVideoTask = (taskId: string) => {
|
||||
return defHttp.get({ url: `${Api.query}/${taskId}` }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据用户id查询视频列表
|
||||
*/
|
||||
export const getVideoListByUser = (params: { userId: string }) => {
|
||||
return defHttp.get({ url: Api.listByUser, params }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除视频记录
|
||||
*/
|
||||
export const deleteVideoRecord = (params) => {
|
||||
return defHttp.delete({ url: Api.deleteRecord, params }, { isTransformResponse: false, joinParamsToUrl: true });
|
||||
};
|
||||
76
jeecgboot-vue3/src/views/super/airag/aivideo/AiVideo.data.ts
Normal file
76
jeecgboot-vue3/src/views/super/airag/aivideo/AiVideo.data.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { FormSchema } from '@/components/Form';
|
||||
import { h } from 'vue';
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
/**
|
||||
* 视频生成表单配置
|
||||
*/
|
||||
export const videoFormSchemas: FormSchema[] = [
|
||||
// {
|
||||
// label: '模型',
|
||||
// field: 'model',
|
||||
// component: 'JDictSelectTag',
|
||||
// required: true,
|
||||
// defaultValue: 'video-generation-1',
|
||||
// componentProps: {
|
||||
// dictCode: "airag_model where model_type = 'VIDEO' and activate_flag = 1,name,id",
|
||||
// placeholder: '请选择视频生成模型',
|
||||
// },
|
||||
// },
|
||||
{
|
||||
label: '视频尺寸',
|
||||
field: 'size',
|
||||
component: 'Select',
|
||||
defaultValue: '1920x1080',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '1280x720 (720P)', value: '1280x720' },
|
||||
{ label: '720x1280', value: '720x1280' },
|
||||
{ label: '1024x1024', value: '1024x1024' },
|
||||
{ label: '1920x1080 (1080P)', value: '1920x1080' },
|
||||
{ label: '1080x1920', value: '1080x1920' },
|
||||
{ label: '2048x1080 (2K)', value: '2048x1080' },
|
||||
{ label: '3840x2160 (4K)', value: '3840x2160' },
|
||||
],
|
||||
placeholder: '请选择视频尺寸',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '视频帧率',
|
||||
field: 'fps',
|
||||
component: 'Select',
|
||||
defaultValue: 30,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '30 FPS', value: 30 },
|
||||
{ label: '60 FPS', value: 60 },
|
||||
],
|
||||
placeholder: '请选择视频帧率',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '视频时长',
|
||||
field: 'duration',
|
||||
component: 'Select',
|
||||
defaultValue: 5,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '5秒', value: 5 },
|
||||
{ label: '10秒', value: 10 },
|
||||
],
|
||||
placeholder: '请选择视频时长',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '是否ai合成音效',
|
||||
field: 'izAiAudio',
|
||||
component: 'Select',
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '否', value: 0 },
|
||||
{ label: '是', value: 1 },
|
||||
],
|
||||
},
|
||||
}
|
||||
];
|
||||
375
jeecgboot-vue3/src/views/super/airag/aivideo/AiVideo.less
Normal file
375
jeecgboot-vue3/src/views/super/airag/aivideo/AiVideo.less
Normal file
@@ -0,0 +1,375 @@
|
||||
// AI Video 页面样式
|
||||
.ai-video-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
background-color: #f0f2f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #8f959e;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
width: 380px;
|
||||
min-width: 320px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.form-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.form-item-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.form-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
:deep(.ant-input-textarea) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.preset-group {
|
||||
margin-bottom: 2px;
|
||||
.preset-label {
|
||||
font-size: 13px;
|
||||
color: #8f959e;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.preset-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preset-item {
|
||||
padding: 6px 14px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #1890ff;
|
||||
background: #fff;
|
||||
color: #1890ff;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn-group {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 8px;
|
||||
border-left: 4px solid #1890ff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
background: #f7f8fc;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.video-info-section {
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin-top: 12px;
|
||||
|
||||
.current-video-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
.info-label {
|
||||
font-size: 13px;
|
||||
color: #8f959e;
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 13px;
|
||||
color: #1f2329;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-panel {
|
||||
width: 400px;
|
||||
min-width: 280px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.history-list-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-history {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #b2b8c6;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.history-item {
|
||||
padding: 12px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
background: #fafbfc;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f9ff;
|
||||
border-color: #d9e8f7;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
gap: 8px;
|
||||
|
||||
.item-title {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: #1f2329;
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.item-time {
|
||||
font-size: 10px;
|
||||
color: #b2b8c6;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: flex-end;
|
||||
|
||||
:deep(.ant-btn) {
|
||||
padding: 0 4px;
|
||||
height: auto;
|
||||
min-width: auto;
|
||||
font-size: 12px;
|
||||
|
||||
&.ant-btn-text {
|
||||
color: #1890ff;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-btn-dangerous.ant-btn-text {
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover {
|
||||
color: #ff7875;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #8f959e;
|
||||
|
||||
p {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
color: #b2b8c6;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.loading-text {
|
||||
text-align: center;
|
||||
color: #595959;
|
||||
|
||||
p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.elapsed-time {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-player-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.video-control {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
415
jeecgboot-vue3/src/views/super/airag/aivideo/AiVideo.vue
Normal file
415
jeecgboot-vue3/src/views/super/airag/aivideo/AiVideo.vue
Normal file
@@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<div class="ai-video-page">
|
||||
<!-- 头部区域 -->
|
||||
<div class="page-header">
|
||||
<div class="title">AI视频</div>
|
||||
<div class="subtitle">将文本快速转换为生动的视频内容</div>
|
||||
</div>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<!-- 左侧:视频控制 + 文案 + 常用场景 + 生成按钮 -->
|
||||
<div class="control-panel">
|
||||
<div class="panel-title">视频配置</div>
|
||||
<div class="form-container">
|
||||
<BasicForm @register="registerForm" :schemas="leftFormSchemas" />
|
||||
|
||||
<!-- 文案输入框 -->
|
||||
<div class="form-item-group">
|
||||
<label class="form-label">文案</label>
|
||||
<a-textarea v-model:value="formText" :rows="4" :maxlength="500" show-count placeholder="请输入要生成视频的文案内容" />
|
||||
</div>
|
||||
|
||||
<!-- 常用场景 -->
|
||||
<div class="preset-group">
|
||||
<div class="preset-label">常用场景</div>
|
||||
<div class="preset-items">
|
||||
<div v-for="(item, index) in presetTexts" :key="index" class="preset-item" @click.prevent="handleApplyPreset(item.content)">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 开始生成按钮 -->
|
||||
<div class="action-btn-group">
|
||||
<a-button type="primary" size="large" block :loading="generating" @click="handleGenerate">
|
||||
<Icon icon="ant-design:video-camera-outlined" />
|
||||
开始生成
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:视频预览 -->
|
||||
<div class="preview-panel">
|
||||
<div class="panel-title">预览区域</div>
|
||||
<div class="preview-content">
|
||||
<div v-if="!currentVideoUrl && !generating" class="empty-state">
|
||||
<Icon icon="ant-design:video-camera-outlined" size="72" color="#c0c4cc" />
|
||||
<p>填写左侧文案并点击「开始生成」</p>
|
||||
<p class="tip">支持调整视频长度、风格和效果,生成更加个性化的视频内容</p>
|
||||
</div>
|
||||
|
||||
<div v-if="generating" class="loading-state">
|
||||
<a-spin size="large" />
|
||||
<div class="loading-text">
|
||||
<p>正在生成视频,请耐心等待...</p>
|
||||
<p class="elapsed-time">已等待 {{ elapsedTimeText }}</p>
|
||||
<p class="status-text">{{ statusText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentVideoUrl && !generating" class="video-player-wrapper">
|
||||
<video ref="videoRef" :src="currentVideoUrl" controls class="video-control" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 当前播放视频信息 -->
|
||||
<div v-if="currentVideoUrl" class="video-info-section">
|
||||
<div class="current-video-info">
|
||||
<span class="info-label">当前视频:</span>
|
||||
<span class="info-text">{{ currentText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧生成历史 - 列表风格 -->
|
||||
<div class="history-panel">
|
||||
<div class="panel-title">生成历史</div>
|
||||
<div class="history-list-wrapper">
|
||||
<div v-if="historyList.length === 0" class="empty-history">
|
||||
<p>暂无生成历史</p>
|
||||
</div>
|
||||
<div v-else class="history-list">
|
||||
<div v-for="(item, index) in historyList" :key="item.id" class="history-item">
|
||||
<div class="item-header">
|
||||
<span class="item-title" :title="item.content">{{ item.content }}</span>
|
||||
<span class="item-time">{{ formatTime(item) }}</span>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<a-button type="text" size="small" @click="handlePlay(item)">
|
||||
<Icon icon="ant-design:play-circle-outlined" />
|
||||
播放
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleUseText(item)">
|
||||
<Icon icon="ant-design:copy-outlined" />
|
||||
复用文案
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleDownload(item)">
|
||||
<Icon icon="ant-design:download-outlined" />
|
||||
下载
|
||||
</a-button>
|
||||
<a-button type="text" size="small" danger @click="handleDelete(item.id)">
|
||||
<Icon icon="ant-design:delete-outlined" />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { BasicForm, useForm } from '@/components/Form';
|
||||
import { Icon } from '@/components/Icon';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { videoFormSchemas } from './AiVideo.data';
|
||||
import { submitVideoTask, queryVideoTask, getVideoListByUser, deleteVideoRecord } from './AiVideo.api';
|
||||
import { getFileAccessHttpUrl } from '@/utils/common/compUtils';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const TASK_ID_KEY = 'ai_video_task_id';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 左侧表单:不包含文案
|
||||
const leftFormSchemas = videoFormSchemas.filter((item) => !['text'].includes(item.field));
|
||||
|
||||
const [registerForm, { validate }] = useForm({
|
||||
schemas: leftFormSchemas,
|
||||
showActionButtonGroup: false,
|
||||
wrapperCol: { span: 24 },
|
||||
labelCol: { span: 24 },
|
||||
});
|
||||
|
||||
const generating = ref(false);
|
||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||
const currentVideoUrl = ref<string>('');
|
||||
const currentText = ref<string>('');
|
||||
const formText = ref<string>('');
|
||||
const historyList = ref<any[]>([]);
|
||||
const isPresetApplying = ref(false);
|
||||
const elapsedSeconds = ref(0);
|
||||
const statusText = ref('任务已提交,排队中...');
|
||||
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let elapsedTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const elapsedTimeText = computed(() => {
|
||||
const minutes = Math.floor(elapsedSeconds.value / 60);
|
||||
const seconds = elapsedSeconds.value % 60;
|
||||
if (minutes > 0) {
|
||||
return `${minutes}分${seconds}秒`;
|
||||
}
|
||||
return `${seconds}秒`;
|
||||
});
|
||||
|
||||
const presetTexts = [
|
||||
{ title: '沙滩金毛犬', content: '一只金毛犬在金色的沙滩上奔跑,海浪轻轻拍打着岸边,阳光明媚,慢动作镜头' },
|
||||
{ title: '航拍山脉全景', content: '航拍壮丽的山脉全景,云雾缭绕在山峰之间,镜头缓缓推进。' },
|
||||
{ title: '咖啡微距特写', content: '一杯咖啡被缓缓倒入透明玻璃杯中,咖啡与牛奶融合形成美丽的纹理,微距特写。' },
|
||||
{ title: '极光延时摄影', content: '星空下的极光在天空中舞动,色彩绚烂,延时摄影效果。' },
|
||||
{
|
||||
title: '女讲师教学',
|
||||
content:
|
||||
'女讲师,站在 PPT 前手持教鞭指向内容,表情认真亲和,讲解自然流畅,手势得体大方,光线明亮清晰,背景干净整洁,1080P 高清,画面稳定流畅,适合知识讲解、课程教学,风格专业、清晰、有说服力',
|
||||
},
|
||||
{
|
||||
title: '口红带货主播',
|
||||
content:
|
||||
'画面主体: 一位美丽的年轻中国女主播,特写镜头,面对镜头微笑。\n' +
|
||||
'外貌着装: 她穿着浅色职业装,妆容精致无瑕。\n' +
|
||||
'核心动作: 她一只手握着一支高端口红,另一只手优雅地打开盖子,露出丝滑的膏体。她温柔地将口红涂抹在自己的嘴唇上,动作轻柔,目光专注且含笑。\n' +
|
||||
'背景环境: 身后是明亮整洁的直播工作室,有环形灯补光,背景呈柔和的虚化效果。\n' +
|
||||
'画面质感: 高色彩饱和度,电影级布光,4k分辨率,60fps。\n' +
|
||||
'特殊效果: 使用慢镜头捕捉口红涂抹瞬间的丝滑质感。\n'+
|
||||
'语言风格:中文,适合电商直播带货场景,风格专业、清晰、有说服力',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 根据当前用户id加载视频列表
|
||||
*/
|
||||
async function loadVideoList() {
|
||||
try {
|
||||
const userId = userStore.getUserInfo?.id;
|
||||
if (!userId) return;
|
||||
const res = await getVideoListByUser({ userId });
|
||||
if (res && res.result) {
|
||||
const list = Array.isArray(res.result) ? res.result : res.result?.records || [];
|
||||
historyList.value = list.map((item) => ({
|
||||
...item,
|
||||
videoFullUrl: item.videoUrl ? getFileAccessHttpUrl(item.videoUrl) : '',
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时获取历史列表
|
||||
loadVideoList();
|
||||
|
||||
/**
|
||||
* 应用预设文案
|
||||
*/
|
||||
function handleApplyPreset(text: string) {
|
||||
if (isPresetApplying.value) return;
|
||||
isPresetApplying.value = true;
|
||||
formText.value = text;
|
||||
setTimeout(() => {
|
||||
isPresetApplying.value = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有定时器
|
||||
*/
|
||||
function clearTimers() {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
if (elapsedTimer) {
|
||||
clearInterval(elapsedTimer);
|
||||
elapsedTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始生成(异步轮询模式)
|
||||
*/
|
||||
async function handleGenerate() {
|
||||
try {
|
||||
const values = await validate();
|
||||
|
||||
if (!formText.value.trim()) {
|
||||
createMessage.warn('请输入文案内容');
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
generating.value = true;
|
||||
currentVideoUrl.value = '';
|
||||
elapsedSeconds.value = 0;
|
||||
statusText.value = '任务已提交,排队中...';
|
||||
values.prompt = formText.value.trim();
|
||||
|
||||
// 启动计时器
|
||||
elapsedTimer = setInterval(() => {
|
||||
elapsedSeconds.value++;
|
||||
}, 1000);
|
||||
|
||||
// 提交任务
|
||||
const submitResult = await submitVideoTask(values);
|
||||
if (!submitResult || !submitResult.success || !submitResult.result?.taskId) {
|
||||
createMessage.error(submitResult?.message || '提交任务失败');
|
||||
generating.value = false;
|
||||
clearTimers();
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = submitResult.result.taskId;
|
||||
localStorage.setItem(TASK_ID_KEY, taskId);
|
||||
statusText.value = '视频生成中...';
|
||||
|
||||
// 开始轮询
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const queryResult = await queryVideoTask(taskId);
|
||||
if (queryResult?.success && queryResult.result) {
|
||||
const { status, videoUrl, message } = queryResult.result;
|
||||
if (status === 'SUCCESS') {
|
||||
clearTimers();
|
||||
generating.value = false;
|
||||
localStorage.removeItem(TASK_ID_KEY);
|
||||
currentVideoUrl.value = getFileAccessHttpUrl(videoUrl) || '';
|
||||
currentText.value = formText.value;
|
||||
createMessage.success('视频生成成功!');
|
||||
await loadVideoList();
|
||||
} else if (status === 'FAIL') {
|
||||
clearTimers();
|
||||
generating.value = false;
|
||||
localStorage.removeItem(TASK_ID_KEY);
|
||||
createMessage.error(message || '视频生成失败');
|
||||
}
|
||||
// PROCESSING 状态继续轮询
|
||||
}
|
||||
} catch (e: any) {
|
||||
clearTimers();
|
||||
generating.value = false;
|
||||
createMessage.error('查询任务状态失败: ' + (e.message || '未知错误'));
|
||||
}
|
||||
}, 5000);
|
||||
} catch (error: any) {
|
||||
clearTimers();
|
||||
generating.value = false;
|
||||
if (error?.errorFields) {
|
||||
return;
|
||||
}
|
||||
createMessage.error(error.message || '提交任务失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从历史记录中播放
|
||||
*/
|
||||
function handlePlay(record: any) {
|
||||
currentVideoUrl.value = record.videoFullUrl || getFileAccessHttpUrl(record.videoUrl) || '';
|
||||
currentText.value = record.content;
|
||||
setTimeout(() => {
|
||||
(videoRef.value as any)?.play?.();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将历史记录的文案回填到文案输入框
|
||||
*/
|
||||
function handleUseText(record: any) {
|
||||
formText.value = record.content;
|
||||
createMessage.success('已将文案填入输入框');
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载视频文件
|
||||
*/
|
||||
function handleDownload(record: any) {
|
||||
const url = record.videoFullUrl || getFileAccessHttpUrl(record.videoUrl);
|
||||
if (!url) {
|
||||
createMessage.error('下载地址不存在');
|
||||
return;
|
||||
}
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = record.fileName || `video-${Date.now()}.mp4`;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除历史记录
|
||||
*/
|
||||
async function handleDelete(id: string) {
|
||||
const userId = userStore.getUserInfo?.id;
|
||||
if (!userId) return;
|
||||
try {
|
||||
const res = await deleteVideoRecord({ userId: userId, recordId: id });
|
||||
if (res.success) {
|
||||
createMessage.success('已删除');
|
||||
await loadVideoList();
|
||||
} else {
|
||||
createMessage.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (e) {
|
||||
createMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间展示
|
||||
*/
|
||||
function formatTime(item: any) {
|
||||
return item?.createTime || '';
|
||||
}
|
||||
|
||||
//update-begin---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
onMounted(() => {
|
||||
const savedTaskId = localStorage.getItem(TASK_ID_KEY);
|
||||
if (savedTaskId) {
|
||||
generating.value = true;
|
||||
currentVideoUrl.value = '';
|
||||
elapsedSeconds.value = 0;
|
||||
statusText.value = '任务恢复中,继续等待...';
|
||||
elapsedTimer = setInterval(() => {
|
||||
elapsedSeconds.value++;
|
||||
}, 1000);
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const queryResult = await queryVideoTask(savedTaskId);
|
||||
if (queryResult?.success && queryResult.result) {
|
||||
const { status, videoUrl, message } = queryResult.result;
|
||||
if (status === 'SUCCESS') {
|
||||
clearTimers();
|
||||
generating.value = false;
|
||||
localStorage.removeItem(TASK_ID_KEY);
|
||||
currentVideoUrl.value = getFileAccessHttpUrl(videoUrl) || '';
|
||||
createMessage.success('视频生成成功!');
|
||||
await loadVideoList();
|
||||
} else if (status === 'FAIL') {
|
||||
clearTimers();
|
||||
generating.value = false;
|
||||
localStorage.removeItem(TASK_ID_KEY);
|
||||
createMessage.error(message || '视频生成失败');
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
clearTimers();
|
||||
generating.value = false;
|
||||
createMessage.error('查询任务状态失败: ' + (e.message || '未知错误'));
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
//update-end---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
onUnmounted(() => {
|
||||
clearTimers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import './AiVideo.less';
|
||||
</style>
|
||||
22
jeecgboot-vue3/src/views/super/airag/aivideo2/AiVideo.api.ts
Normal file
22
jeecgboot-vue3/src/views/super/airag/aivideo2/AiVideo.api.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
|
||||
enum Api {
|
||||
submit = '/airag/video/submit',
|
||||
query = '/airag/video/query',
|
||||
prompts = '/airag/video/prompts',
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交视频生成任务
|
||||
*/
|
||||
export const submitVideoTask = (params: { prompt: string; category?: string }) => defHttp.post({ url: Api.submit, params });
|
||||
|
||||
/**
|
||||
* 查询视频生成任务状态
|
||||
*/
|
||||
export const queryVideoTask = (taskId: string) => defHttp.get({ url: `${Api.query}/${taskId}` });
|
||||
|
||||
/**
|
||||
* 获取预设提示词
|
||||
*/
|
||||
export const getPresetPrompts = () => defHttp.get({ url: Api.prompts });
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { FormSchema } from '@/components/Form';
|
||||
|
||||
/**
|
||||
* 视频生成表单Schema
|
||||
*/
|
||||
export const videoFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'prompt',
|
||||
label: '视频描述',
|
||||
component: 'InputTextArea',
|
||||
required: true,
|
||||
componentProps: {
|
||||
placeholder: '请描述你想生成的视频内容,例如:一只金毛犬在沙滩上奔跑,海浪拍打岸边,阳光明媚',
|
||||
rows: 5,
|
||||
maxlength: 500,
|
||||
showCount: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 场景分类
|
||||
*/
|
||||
export const categoryList = ['通用演示', '产品营销', '教育培训', '创意设计'];
|
||||
|
||||
/**
|
||||
* 备用预设提示词(当后端API不可用时使用)
|
||||
*/
|
||||
export const fallbackPrompts: Record<string, string[]> = {
|
||||
通用演示: [
|
||||
'一只金毛犬在金色的沙滩上奔跑,海浪轻轻拍打着岸边,阳光明媚,慢动作镜头',
|
||||
'航拍壮丽的山脉全景,云雾缭绕在山峰之间,镜头缓缓推进',
|
||||
'樱花树下,花瓣随风飘落,一条小溪静静流淌,春日午后的宁静氛围',
|
||||
],
|
||||
产品营销: [
|
||||
'一杯咖啡被缓缓倒入透明玻璃杯中,咖啡与牛奶融合形成美丽的纹理,微距特写',
|
||||
'一款高端智能手表在旋转展示台上缓缓旋转,灯光打在表面上反射出金属光泽,黑色背景',
|
||||
'一双运动鞋踩入水洼溅起水花,慢动作特写,动感活力的画面',
|
||||
],
|
||||
教育培训: [
|
||||
'地球从太空视角缓缓旋转,可以看到大气层和云层的细节,星空背景',
|
||||
'一本书的书页被风吹动快速翻动,文字和插图若隐若现,知识流动的意象',
|
||||
'显微镜下的细胞分裂过程,色彩鲜明的科学可视化风格',
|
||||
],
|
||||
创意设计: [
|
||||
'一座未来主义的城市在日落时分,霓虹灯光倒映在雨水的路面上,赛博朋克风格',
|
||||
'水墨在水中缓缓扩散,形成抽象的山水画意境,中国风艺术效果',
|
||||
'星空下的极光在天空中舞动,色彩绚烂,延时摄影效果',
|
||||
],
|
||||
};
|
||||
418
jeecgboot-vue3/src/views/super/airag/aivideo2/AiVideo.vue
Normal file
418
jeecgboot-vue3/src/views/super/airag/aivideo2/AiVideo.vue
Normal file
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<div class="content-wrapper">
|
||||
<!-- 左侧配置面板 -->
|
||||
<div class="config-panel">
|
||||
<div class="config-tabs">
|
||||
<a-tabs v-model:activeKey="activeCategory" :tabBarStyle="{ margin: 0 }">
|
||||
<a-tab-pane v-for="cat in categoryList" :key="cat" :tab="cat" />
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 预设提示词 -->
|
||||
<div class="preset-prompts">
|
||||
<div class="preset-label">快捷提示词</div>
|
||||
<div class="preset-list">
|
||||
<a-button
|
||||
v-for="(prompt, index) in currentPrompts"
|
||||
:key="index"
|
||||
class="preset-btn"
|
||||
size="small"
|
||||
@click="applyPrompt(prompt)"
|
||||
>
|
||||
{{ prompt.length > 20 ? prompt.substring(0, 20) + '...' : prompt }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-container">
|
||||
<BasicForm @register="registerForm" />
|
||||
</div>
|
||||
|
||||
<div class="action-container">
|
||||
<a-button type="primary" size="large" block @click="handleGenerate" :loading="generating" :disabled="generating">
|
||||
<Icon icon="ant-design:video-camera-outlined" />
|
||||
{{ generating ? '生成中...' : '开始生成' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧预览面板 -->
|
||||
<div class="preview-panel">
|
||||
<div class="panel-title">生成结果</div>
|
||||
<div class="preview-content">
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!videoUrl && !generating" class="empty-state">
|
||||
<Icon icon="ant-design:video-camera-outlined" size="64" color="#ccc" />
|
||||
<p>在左侧输入视频描述,点击开始生成</p>
|
||||
</div>
|
||||
|
||||
<!-- 生成中 -->
|
||||
<div v-if="generating" class="loading-state">
|
||||
<a-spin size="large" />
|
||||
<div class="loading-text">
|
||||
<p>正在生成视频,请耐心等待...</p>
|
||||
<p class="elapsed-time">已等待 {{ elapsedTimeText }}</p>
|
||||
<p class="status-text">{{ statusText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 生成完成 -->
|
||||
<div v-if="videoUrl && !generating" class="result-video-wrapper">
|
||||
<video :src="videoUrl" controls class="result-video" />
|
||||
<div class="video-actions">
|
||||
<a-button type="primary" @click="handleDownload">
|
||||
<Icon icon="ant-design:download-outlined" />
|
||||
下载视频
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<Icon icon="ant-design:redo-outlined" />
|
||||
重新生成
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 生成失败 -->
|
||||
<div v-if="errorMessage && !generating" class="error-state">
|
||||
<Icon icon="ant-design:close-circle-outlined" size="64" color="#ff4d4f" />
|
||||
<p class="error-text">{{ errorMessage }}</p>
|
||||
<a-button type="primary" @click="handleReset">重试</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onUnmounted } from 'vue';
|
||||
import { BasicForm, useForm } from '@/components/Form';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { Icon } from '@/components/Icon';
|
||||
import { submitVideoTask, queryVideoTask, getPresetPrompts } from './AiVideo.api';
|
||||
import { videoFormSchema, categoryList, fallbackPrompts } from './AiVideo.data';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const activeCategory = ref('通用演示');
|
||||
const generating = ref(false);
|
||||
const videoUrl = ref('');
|
||||
const errorMessage = ref('');
|
||||
const elapsedSeconds = ref(0);
|
||||
const statusText = ref('任务已提交,排队中...');
|
||||
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let elapsedTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// 预设提示词(优先从后端加载)
|
||||
const promptsMap = ref<Record<string, string[]>>({ ...fallbackPrompts });
|
||||
|
||||
// 加载后端预设提示词
|
||||
getPresetPrompts()
|
||||
.then((data) => {
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
promptsMap.value = data;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 使用备用提示词
|
||||
});
|
||||
|
||||
const currentPrompts = computed(() => {
|
||||
return promptsMap.value[activeCategory.value] || [];
|
||||
});
|
||||
|
||||
const elapsedTimeText = computed(() => {
|
||||
const minutes = Math.floor(elapsedSeconds.value / 60);
|
||||
const seconds = elapsedSeconds.value % 60;
|
||||
if (minutes > 0) {
|
||||
return `${minutes}分${seconds}秒`;
|
||||
}
|
||||
return `${seconds}秒`;
|
||||
});
|
||||
|
||||
const [registerForm, { validate, setFieldsValue }] = useForm({
|
||||
schemas: videoFormSchema,
|
||||
labelWidth: 100,
|
||||
actionColOptions: { span: 24 },
|
||||
showActionButtonGroup: false,
|
||||
});
|
||||
|
||||
function applyPrompt(prompt: string) {
|
||||
setFieldsValue({ prompt });
|
||||
}
|
||||
|
||||
async function handleGenerate() {
|
||||
try {
|
||||
const values = await validate();
|
||||
if (!values.prompt || !values.prompt.trim()) {
|
||||
createMessage.warning('请输入视频描述');
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
generating.value = true;
|
||||
videoUrl.value = '';
|
||||
errorMessage.value = '';
|
||||
elapsedSeconds.value = 0;
|
||||
statusText.value = '任务已提交,排队中...';
|
||||
|
||||
// 启动计时器
|
||||
elapsedTimer = setInterval(() => {
|
||||
elapsedSeconds.value++;
|
||||
}, 1000);
|
||||
|
||||
// 提交任务
|
||||
const submitResult = await submitVideoTask({
|
||||
prompt: values.prompt.trim(),
|
||||
category: activeCategory.value,
|
||||
});
|
||||
|
||||
if (!submitResult || !submitResult.taskId) {
|
||||
throw new Error(submitResult?.message || '提交任务失败');
|
||||
}
|
||||
|
||||
statusText.value = '视频生成中...';
|
||||
|
||||
// 开始轮询
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const queryResult = await queryVideoTask(submitResult.taskId);
|
||||
if (queryResult.status === 'SUCCESS') {
|
||||
clearTimers();
|
||||
generating.value = false;
|
||||
videoUrl.value = queryResult.videoUrl;
|
||||
createMessage.success('视频生成成功!');
|
||||
} else if (queryResult.status === 'FAIL') {
|
||||
clearTimers();
|
||||
generating.value = false;
|
||||
errorMessage.value = queryResult.message || '视频生成失败';
|
||||
}
|
||||
// PROCESSING状态继续轮询
|
||||
} catch (e: any) {
|
||||
clearTimers();
|
||||
generating.value = false;
|
||||
errorMessage.value = '查询任务状态失败: ' + (e.message || '未知错误');
|
||||
}
|
||||
}, 5000);
|
||||
} catch (error: any) {
|
||||
clearTimers();
|
||||
generating.value = false;
|
||||
if (error?.errorFields) {
|
||||
// 表单验证失败,不显示额外错误
|
||||
return;
|
||||
}
|
||||
errorMessage.value = error.message || '提交任务失败';
|
||||
}
|
||||
}
|
||||
|
||||
function clearTimers() {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
if (elapsedTimer) {
|
||||
clearInterval(elapsedTimer);
|
||||
elapsedTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
videoUrl.value = '';
|
||||
errorMessage.value = '';
|
||||
elapsedSeconds.value = 0;
|
||||
}
|
||||
|
||||
function handleDownload() {
|
||||
if (!videoUrl.value) return;
|
||||
const a = document.createElement('a');
|
||||
a.href = videoUrl.value;
|
||||
a.download = `ai-video-${Date.now()}.mp4`;
|
||||
a.target = '_blank';
|
||||
a.click();
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.config-panel {
|
||||
width: 550px;
|
||||
min-width: 350px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.config-tabs {
|
||||
margin-bottom: 16px;
|
||||
|
||||
:deep(.ant-tabs-nav::before) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-tab) {
|
||||
padding: 8px 0;
|
||||
margin: 0 24px 0 0;
|
||||
font-size: 15px;
|
||||
|
||||
&.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-ink-bar) {
|
||||
background: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.preset-prompts {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.preset-label {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.preset-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
.preset-btn {
|
||||
color: #595959;
|
||||
border-color: #d9d9d9;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
background: #f7f8fc;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
margin-bottom: 20px;
|
||||
padding-left: 8px;
|
||||
border-left: 4px solid #1890ff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #8f959e;
|
||||
|
||||
p {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.loading-text {
|
||||
text-align: center;
|
||||
color: #595959;
|
||||
|
||||
p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.elapsed-time {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-video-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
|
||||
.result-video {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-height: calc(100% - 60px);
|
||||
border-radius: 8px;
|
||||
background: #000;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.video-actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.error-state {
|
||||
text-align: center;
|
||||
|
||||
.error-text {
|
||||
margin: 16px 0;
|
||||
color: #ff4d4f;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
jeecgboot-vue3/src/views/super/airag/aivoice/AiVoice.api.ts
Normal file
34
jeecgboot-vue3/src/views/super/airag/aivoice/AiVoice.api.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
|
||||
enum Api {
|
||||
generate = '/airag/voice/generate',
|
||||
generateAsync = '/airag/voice/generateAsync',
|
||||
queryTask = '/airag/voice/queryTask',
|
||||
listByUser = '/airag/voice/listByUser',
|
||||
deleteVoiceRecord = '/airag/voice/deleteVoiceRecord',
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交语音生成任务(同步,保留兼容)
|
||||
*/
|
||||
export const submitVoiceTask = (params) => defHttp.post({ url: Api.generate, params }, { isTransformResponse: false });
|
||||
|
||||
/**
|
||||
* 异步提交语音生成任务,立即返回 taskId
|
||||
*/
|
||||
export const generateVoiceAsync = (params) => defHttp.post({ url: Api.generateAsync, params }, { isTransformResponse: false });
|
||||
|
||||
/**
|
||||
* 查询异步语音任务结果
|
||||
*/
|
||||
export const queryVoiceTask = (taskId: string) => defHttp.get({ url: `${Api.queryTask}/${taskId}` }, { isTransformResponse: false });
|
||||
|
||||
/**
|
||||
* 根据用户id查询语音列表
|
||||
*/
|
||||
export const getVoiceListByUser = (params: { userId: string }) => defHttp.get({ url: Api.listByUser, params },{ isTransformResponse: false });
|
||||
|
||||
/**
|
||||
* 删除语音记录
|
||||
*/
|
||||
export const deleteVoiceRecord = (params) => defHttp.delete({ url: Api.deleteVoiceRecord, params }, { isTransformResponse: false, joinParamsToUrl: true });
|
||||
117
jeecgboot-vue3/src/views/super/airag/aivoice/AiVoice.data.ts
Normal file
117
jeecgboot-vue3/src/views/super/airag/aivoice/AiVoice.data.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { FormSchema } from '@/components/Form';
|
||||
|
||||
// 左侧语音控制表单
|
||||
export const voiceFormSchemas: FormSchema[] = [
|
||||
/* {
|
||||
label: '模型',
|
||||
field: 'model',
|
||||
component: 'JDictSelectTag',
|
||||
required: true,
|
||||
defaultValue: 'voice-generation-1',
|
||||
componentProps: {
|
||||
placeholder: '请选择语音模型',
|
||||
dictCode: "airag_model where model_type = 'VOICE' and activate_flag = 1,name,id",
|
||||
},
|
||||
},*/
|
||||
{
|
||||
label: '倍速',
|
||||
field: 'speed',
|
||||
component: 'Slider',
|
||||
defaultValue: 1,
|
||||
colProps: {
|
||||
span: 24,
|
||||
},
|
||||
componentProps: {
|
||||
min: 0.25,
|
||||
max: 4,
|
||||
step: 0.1,
|
||||
marks: {
|
||||
0.5: '0.5x',
|
||||
1: '1x',
|
||||
1.5: '1.5x',
|
||||
2: '2x',
|
||||
3: '3x',
|
||||
4: '4x',
|
||||
},
|
||||
tooltip: {
|
||||
formatter: (value: number) => `${value.toFixed(1)}x`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '音量增益(dB)',
|
||||
field: 'volume',
|
||||
component: 'Slider',
|
||||
defaultValue: 0,
|
||||
colProps: {
|
||||
span: 24,
|
||||
},
|
||||
componentProps: {
|
||||
min: -10,
|
||||
max: 10,
|
||||
step: 1,
|
||||
marks: {
|
||||
'-10': '-10',
|
||||
0: '0',
|
||||
10: '10',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '声色',
|
||||
field: 'voice',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
defaultValue: 'tongtong',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '彤彤', value: 'tongtong' },
|
||||
{ label: '锤锤', value: 'chuichui' },
|
||||
{ label: '小陈', value: 'xiaochen' },
|
||||
{ label: 'Jam', value: 'jam' },
|
||||
{ label: 'Kazi', value: 'kazi' },
|
||||
{ label: 'Douji', value: 'douji' },
|
||||
{ label: 'Luodo', value: 'luodo' },
|
||||
],
|
||||
placeholder: '请选择声色',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '文案',
|
||||
field: 'text',
|
||||
component: 'InputTextArea',
|
||||
required: true,
|
||||
colProps: {
|
||||
span: 24,
|
||||
},
|
||||
componentProps: {
|
||||
rows: 6,
|
||||
placeholder: '请输入要合成的文案内容',
|
||||
maxlength: 500,
|
||||
showCount: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 历史记录表格列配置
|
||||
*/
|
||||
export const historyColumns = [
|
||||
{
|
||||
title: '文案',
|
||||
dataIndex: 'text',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
382
jeecgboot-vue3/src/views/super/airag/aivoice/AiVoice.less
Normal file
382
jeecgboot-vue3/src/views/super/airag/aivoice/AiVoice.less
Normal file
@@ -0,0 +1,382 @@
|
||||
// AI Voice 样式文件
|
||||
.ai-voice-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
background-color: #f0f2f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #8f959e;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
width: 300px;
|
||||
min-width: 260px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.form-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 中间区域:试听 + 文案 + 常用场景 + 生成按钮
|
||||
.middle-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
.preview-panel {
|
||||
flex: 0 0 auto;
|
||||
height: 300px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 8px;
|
||||
border-left: 4px solid #1890ff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
background: #f7f8fc;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.audio-info-section {
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin-top: 12px;
|
||||
|
||||
.current-audio-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
.info-label {
|
||||
font-size: 13px;
|
||||
color: #8f959e;
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 13px;
|
||||
color: #1f2329;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-section {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.form-item-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.form-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
:deep(.ant-input-textarea) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.preset-group {
|
||||
.preset-label {
|
||||
font-size: 13px;
|
||||
color: #8f959e;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.preset-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preset-item {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 14px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #1890ff;
|
||||
background: #fff;
|
||||
color: #1890ff;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn-group {
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-panel {
|
||||
width: 400px;
|
||||
min-width: 280px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.history-list-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-history {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #b2b8c6;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.history-item {
|
||||
padding: 12px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
background: #fafbfc;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f9ff;
|
||||
border-color: #d9e8f7;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
gap: 8px;
|
||||
|
||||
.item-title {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: #1f2329;
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.item-time {
|
||||
font-size: 10px;
|
||||
color: #b2b8c6;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: flex-end;
|
||||
|
||||
:deep(.ant-btn) {
|
||||
padding: 0 4px;
|
||||
height: auto;
|
||||
min-width: auto;
|
||||
font-size: 12px;
|
||||
|
||||
&.ant-btn-text {
|
||||
color: #1890ff;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-btn-dangerous.ant-btn-text {
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover {
|
||||
color: #ff7875;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
margin-bottom: 20px;
|
||||
padding-left: 8px;
|
||||
border-left: 4px solid #1890ff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #8f959e;
|
||||
|
||||
p {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
color: #b2b8c6;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-player-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
332
jeecgboot-vue3/src/views/super/airag/aivoice/AiVoice.vue
Normal file
332
jeecgboot-vue3/src/views/super/airag/aivoice/AiVoice.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<template>
|
||||
<div class="ai-voice-page">
|
||||
<!-- 头部区域 -->
|
||||
<div class="page-header">
|
||||
<div class="title">AI语音</div>
|
||||
<div class="subtitle">将文本快速转换为自然流畅的语音</div>
|
||||
</div>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<!-- 左侧语音控制 -->
|
||||
<div class="control-panel">
|
||||
<div class="panel-title">语音控制</div>
|
||||
<div class="form-container">
|
||||
<BasicForm @register="registerForm" :schemas="leftFormSchemas" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间区域:试听 + 文案 + 常用场景 + 生成按钮 -->
|
||||
<div class="middle-wrapper">
|
||||
<!-- 试听区域 -->
|
||||
<div class="preview-panel">
|
||||
<div class="panel-title">试听区域</div>
|
||||
<div class="preview-content">
|
||||
<div v-if="!currentAudioUrl" class="empty-state">
|
||||
<Icon icon="ant-design:customer-service-outlined" size="72" color="#c0c4cc" />
|
||||
<p>填写下方文案并点击「开始合成」</p>
|
||||
<p class="tip">支持调整倍速、音量增益和声色,生成更加个性化的语音效果</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="audio-player-wrapper">
|
||||
<audio ref="audioRef" :src="currentAudioUrl" controls class="audio-control" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 当前播放音频信息 -->
|
||||
<div v-if="currentAudioUrl" class="audio-info-section">
|
||||
<div class="current-audio-info">
|
||||
<span class="info-label">当前语音:</span>
|
||||
<span class="info-text">{{ currentText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文案输入和常用场景 -->
|
||||
<div class="input-section">
|
||||
<!-- 文案输入框 -->
|
||||
<div class="form-item-group">
|
||||
<label class="form-label">文案</label>
|
||||
<a-textarea v-model:value="formText" :rows="4" :maxlength="500" show-count placeholder="请输入要合成的文案内容" />
|
||||
</div>
|
||||
|
||||
<!-- 常用场景 -->
|
||||
<div class="preset-group">
|
||||
<div class="preset-label">常用场景</div>
|
||||
<div class="preset-items">
|
||||
<div v-for="item in presetTexts" :key="item" class="preset-item" @click.prevent="handleApplyPreset(item)">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 开始合成按钮 -->
|
||||
<div class="action-btn-group">
|
||||
<a-button type="primary" size="large" block :loading="generating" @click="handleSynthesize">
|
||||
<Icon icon="ant-design:sound-outlined" />
|
||||
开始合成
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧生成历史 - 列表风格 -->
|
||||
<div class="history-panel">
|
||||
<div class="panel-title">生成历史</div>
|
||||
<div class="history-list-wrapper">
|
||||
<div v-if="historyList.length === 0" class="empty-history">
|
||||
<p>暂无生成历史</p>
|
||||
</div>
|
||||
<div v-else class="history-list">
|
||||
<div v-for="item in historyList" :key="item.id" class="history-item">
|
||||
<div class="item-header">
|
||||
<span class="item-title" :title="item.content">{{ item.content }}</span>
|
||||
<span class="item-time">{{ item.createTime }}</span>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<a-button type="text" size="small" @click="handlePlay(item)">
|
||||
<Icon icon="ant-design:sound-outlined" />
|
||||
播放
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleUseText(item)">
|
||||
<Icon icon="ant-design:copy-outlined" />
|
||||
复用文案
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleDownload(item)">
|
||||
<Icon icon="ant-design:download-outlined" />
|
||||
下载
|
||||
</a-button>
|
||||
<a-button type="text" size="small" danger @click="handleDelete(item.id)">
|
||||
<Icon icon="ant-design:delete-outlined" />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { BasicForm, useForm } from '@/components/Form';
|
||||
import { Icon } from '@/components/Icon';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { voiceFormSchemas } from './AiVoice.data';
|
||||
import { generateVoiceAsync, queryVoiceTask, getVoiceListByUser, deleteVoiceRecord } from '@/views/super/airag/aivoice/AiVoice.api';
|
||||
import { getFileAccessHttpUrl } from '@/utils/common/compUtils';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const TASK_ID_KEY = 'ai_voice_task_id';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 左侧表单:只包含模型、倍速、音量、声色,不包含文案
|
||||
const leftFormSchemas = voiceFormSchemas.filter((item) => !['text'].includes(item.field));
|
||||
|
||||
const [registerForm, { validate }] = useForm({
|
||||
schemas: leftFormSchemas,
|
||||
showActionButtonGroup: false,
|
||||
wrapperCol: { span: 24 },
|
||||
labelCol: { span: 24 },
|
||||
});
|
||||
|
||||
const generating = ref(false);
|
||||
const audioRef = ref<HTMLAudioElement | null>(null);
|
||||
const currentAudioUrl = ref<string>('');
|
||||
const currentText = ref<string>('');
|
||||
const formText = ref<string>(''); // 文案输入框的独立状态
|
||||
const historyList = ref<any[]>([]);
|
||||
const isPresetApplying = ref(false); // 防抖标志
|
||||
let pollTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/**
|
||||
* 根据当前用户id加载语音列表
|
||||
*/
|
||||
async function loadVoiceList() {
|
||||
try {
|
||||
const userId = userStore.getUserInfo?.id;
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
await getVoiceListByUser({ userId }).then((res) =>{
|
||||
if(res && res.result){
|
||||
for (const rs of res.result) {
|
||||
if (rs.voiceUrl) {
|
||||
rs.audioUrl = getFileAccessHttpUrl(rs.voiceUrl) || '';
|
||||
}
|
||||
}
|
||||
historyList.value = res.result || [];
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时获取历史列表
|
||||
loadVoiceList();
|
||||
|
||||
const presetTexts = [
|
||||
'欢迎来到我们的平台,祝您使用愉快!',
|
||||
'今天的天气非常好,适合出门散步。',
|
||||
'尊敬的客户,您的订单已发货,请注意查收。',
|
||||
'Welcome to our platform, we hope you enjoy the experience!',
|
||||
];
|
||||
|
||||
/**
|
||||
* 应用预设文案
|
||||
*/
|
||||
function handleApplyPreset(text: string) {
|
||||
if (isPresetApplying.value) return;
|
||||
|
||||
isPresetApplying.value = true;
|
||||
formText.value = text;
|
||||
|
||||
// 300ms 后取消防抖标志
|
||||
setTimeout(() => {
|
||||
isPresetApplying.value = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始合成(异步轮询模式)
|
||||
*/
|
||||
async function handleSynthesize() {
|
||||
try {
|
||||
const values = await validate();
|
||||
|
||||
if (!formText.value.trim()) {
|
||||
createMessage.warn('请输入文案内容');
|
||||
return;
|
||||
}
|
||||
|
||||
generating.value = true;
|
||||
currentAudioUrl.value = '';
|
||||
values.content = formText.value.trim();
|
||||
|
||||
//update-begin---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
const res = await generateVoiceAsync(values);
|
||||
if (res.success && res.result) {
|
||||
const taskId = res.result as string;
|
||||
localStorage.setItem(TASK_ID_KEY, taskId);
|
||||
startPolling(taskId);
|
||||
} else {
|
||||
generating.value = false;
|
||||
createMessage.error(res.message || '提交任务失败');
|
||||
}
|
||||
} catch (e) {
|
||||
generating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 轮询查询语音任务结果 */
|
||||
function startPolling(taskId: string) {
|
||||
const poll = () => {
|
||||
queryVoiceTask(taskId)
|
||||
//update-end---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
//update-begin---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
if (res.result === 'pending' || res.result === null) {
|
||||
pollTimer = setTimeout(poll, 3000);
|
||||
} else {
|
||||
generating.value = false;
|
||||
localStorage.removeItem(TASK_ID_KEY);
|
||||
currentAudioUrl.value = getFileAccessHttpUrl(res.result.voiceUrl) || '';
|
||||
currentText.value = formText.value;
|
||||
createMessage.success('语音合成完成');
|
||||
loadVoiceList();
|
||||
}
|
||||
} else {
|
||||
generating.value = false;
|
||||
localStorage.removeItem(TASK_ID_KEY);
|
||||
createMessage.error(res.message || '语音合成失败');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
pollTimer = setTimeout(poll, 3000);
|
||||
});
|
||||
};
|
||||
poll();
|
||||
//update-end---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
}
|
||||
|
||||
//update-begin---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
onMounted(() => {
|
||||
const savedTaskId = localStorage.getItem(TASK_ID_KEY);
|
||||
if (savedTaskId) {
|
||||
generating.value = true;
|
||||
currentAudioUrl.value = '';
|
||||
startPolling(savedTaskId);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollTimer) {
|
||||
clearTimeout(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
});
|
||||
//update-end---wangshuai---date:20260415 for:[QQYUN-14944]AI 改成异步的,支持切换菜单------------
|
||||
|
||||
/**
|
||||
* 从历史记录中播放
|
||||
*/
|
||||
function handlePlay(record: any) {
|
||||
currentAudioUrl.value = getFileAccessHttpUrl(record.voiceUrl) || '';
|
||||
currentText.value = record.content;
|
||||
setTimeout(() => {
|
||||
(audioRef.value as any)?.play?.();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将历史记录的文案回填到文案输入框
|
||||
*/
|
||||
function handleUseText(record: any) {
|
||||
formText.value = record.content;
|
||||
createMessage.success('已将文案填入输入框');
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载语音文件
|
||||
*/
|
||||
function handleDownload(record: any) {
|
||||
const url = getFileAccessHttpUrl(record.voiceUrl);
|
||||
if (!url) {
|
||||
createMessage.error('下载地址不存在');
|
||||
return;
|
||||
}
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = record.fileName || 'voice.wav';
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除历史记录
|
||||
*/
|
||||
async function handleDelete(id: string) {
|
||||
const userId = userStore.getUserInfo?.id;
|
||||
if (!userId) return;
|
||||
try {
|
||||
const res = await deleteVoiceRecord({ userId: userId, recordId: id });
|
||||
if (res.success) {
|
||||
createMessage.success('已删除');
|
||||
loadVoiceList();
|
||||
} else {
|
||||
createMessage.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (e) {
|
||||
createMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import './AiVoice.less';
|
||||
</style>
|
||||
125
jeecgboot-vue3/src/views/super/online/cgform/CgformCopyList.vue
Normal file
125
jeecgboot-vue3/src/views/super/online/cgform/CgformCopyList.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<!--引用表格-->
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-button @click="onShowCustomButton" type="primary" preIcon="ant-design:highlight">自定义按钮</a-button>
|
||||
<a-button @click="onShowEnhanceJs" type="primary" preIcon="ant-design:strikethrough">JS增强</a-button>
|
||||
<a-button @click="onShowEnhanceSql" type="primary" preIcon="ant-design:filter">SQL增强</a-button>
|
||||
<a-button @click="onShowEnhanceJava" type="primary" preIcon="ant-design:tool">Java增强</a-button>
|
||||
</template>
|
||||
|
||||
<template #dbSync="{ text }">
|
||||
<span v-if="text === 'Y'" style="color: limegreen">已同步</span>
|
||||
<span v-if="text === 'N'" style="color: red">未同步</span>
|
||||
</template>
|
||||
|
||||
<!--操作栏-->
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<CgformModal @register="registerCgformModal" :actionButton="false" @success="reload" />
|
||||
<EnhanceJsModal @register="registerEnhanceJsModal" />
|
||||
<EnhanceJavaModal @register="registerEnhanceJavaModal" />
|
||||
<EnhanceSqlModal @register="registerEnhanceSqlModal" />
|
||||
<DbToOnlineModal @register="registerDbToOnlineModal" @success="reload" />
|
||||
<CustomButtonList @register="registerCustomButtonModal" />
|
||||
<AuthManagerDrawer @register="registerAuthManagerDrawer" />
|
||||
<AuthSetterModal @register="registerAuthSetterModal" />
|
||||
<CgformAddressModal @register="registerAddressModal" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { watch, provide, defineComponent } from 'vue';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import CgformModal from './components/CgformModal.vue';
|
||||
import DbToOnlineModal from './components/DbToOnlineModal.vue';
|
||||
import CustomButtonList from './components/button/CustomButtonList.vue';
|
||||
import EnhanceJsModal from './components/enhance/EnhanceJsModal.vue';
|
||||
import EnhanceJavaModal from './components/enhance/EnhanceJavaModal.vue';
|
||||
import EnhanceSqlModal from './components/enhance/EnhanceSqlModal.vue';
|
||||
import AuthManagerDrawer from './components/auth/AuthManagerDrawer.vue';
|
||||
import AuthSetterModal from './components/auth/AuthSetterModal.vue';
|
||||
import CgformAddressModal from "./components/CgformAddressModal.vue";
|
||||
import { useCgformList } from './hooks/useCgformList';
|
||||
import { CgformPageType } from './types';
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export default defineComponent({
|
||||
name: 'CgformCopyList',
|
||||
components: {
|
||||
BasicTable,
|
||||
TableAction,
|
||||
CgformModal,
|
||||
DbToOnlineModal,
|
||||
CustomButtonList,
|
||||
EnhanceJsModal,
|
||||
EnhanceJavaModal,
|
||||
EnhanceSqlModal,
|
||||
AuthManagerDrawer,
|
||||
AuthSetterModal,
|
||||
CgformAddressModal,
|
||||
},
|
||||
setup() {
|
||||
const pageType = CgformPageType.copy;
|
||||
provide('cgformPageType', pageType);
|
||||
const {
|
||||
router,
|
||||
pageContext,
|
||||
getTableAction,
|
||||
getDropDownAction,
|
||||
onShowCustomButton,
|
||||
onShowEnhanceJs,
|
||||
onShowEnhanceSql,
|
||||
onShowEnhanceJava,
|
||||
registerCustomButtonModal,
|
||||
registerEnhanceJsModal,
|
||||
registerEnhanceSqlModal,
|
||||
registerEnhanceJavaModal,
|
||||
registerAuthManagerDrawer,
|
||||
registerAuthSetterModal,
|
||||
registerCgformModal,
|
||||
registerDbToOnlineModal,
|
||||
registerAddressModal,
|
||||
} = useCgformList({
|
||||
pageType,
|
||||
designScope: 'online-cgform-list',
|
||||
columns: [
|
||||
{ title: '视图表名', dataIndex: 'tableName' },
|
||||
{ title: '视图表描述', dataIndex: 'tableTxt' },
|
||||
{ title: '原表版本', dataIndex: 'copyVersion' },
|
||||
{ title: '视图版本', dataIndex: 'tableVersion' },
|
||||
],
|
||||
formSchemas: [{ label: '表名', field: 'tableName', component: 'JInput' }],
|
||||
});
|
||||
const { prefixCls, tableContext } = pageContext;
|
||||
const [registerTable, { reload }, { rowSelection }] = tableContext;
|
||||
watch(router.currentRoute, () => reload());
|
||||
return {
|
||||
prefixCls,
|
||||
reload,
|
||||
rowSelection,
|
||||
getTableAction,
|
||||
getDropDownAction,
|
||||
onShowCustomButton,
|
||||
onShowEnhanceJs,
|
||||
onShowEnhanceSql,
|
||||
onShowEnhanceJava,
|
||||
registerCustomButtonModal,
|
||||
registerEnhanceJsModal,
|
||||
registerEnhanceSqlModal,
|
||||
registerEnhanceJavaModal,
|
||||
registerAuthManagerDrawer,
|
||||
registerAuthSetterModal,
|
||||
registerTable,
|
||||
registerCgformModal,
|
||||
registerDbToOnlineModal,
|
||||
registerAddressModal,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<!-- online查询条件中的下拉搜索 -->
|
||||
<template>
|
||||
<a-select
|
||||
:value="selected"
|
||||
:placeholder="placeholder"
|
||||
show-search
|
||||
:default-active-first-option="false"
|
||||
:show-arrow="true"
|
||||
:filter-option="false"
|
||||
:not-found-content="null"
|
||||
@search="handleSearch"
|
||||
@change="handleChange"
|
||||
@popupScroll="handlePopupScroll"
|
||||
allowClear
|
||||
>
|
||||
<a-select-option v-for="d in selectOptions" :key="d.value">
|
||||
{{ d.text }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
const { createMessage: $message } = useMessage();
|
||||
import { watch, ref } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'JOnlineSearchSelect',
|
||||
props: {
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
// online CgReport item id
|
||||
fieldId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:value'],
|
||||
setup(props, { emit }) {
|
||||
let selected = ref('');
|
||||
let selectOptions = ref([]);
|
||||
let isHasData = true;
|
||||
let scrollLoading = false;
|
||||
let searchKeyword = '';
|
||||
const pageNo = ref(1);
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
selected.value = undefined;
|
||||
} else {
|
||||
selected.value = newVal;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.fieldId,
|
||||
() => {
|
||||
resetOptions();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
/**
|
||||
* 2024-07-17
|
||||
* liaozhiyang
|
||||
* 【TV360X-1813】online报表查询支持滚动加载
|
||||
* */
|
||||
const handleSearch = useDebounceFn((keyword) => {
|
||||
searchKeyword = keyword;
|
||||
pageNo.value = 1;
|
||||
isHasData = true;
|
||||
searchByKeyword(keyword);
|
||||
}, 800);
|
||||
|
||||
/**
|
||||
* 2024-07-17
|
||||
* liaozhiyang
|
||||
* 【TV360X-1813】online报表查询支持滚动加载
|
||||
* */
|
||||
async function searchByKeyword(keyword = '') {
|
||||
let params = {
|
||||
keyword: keyword,
|
||||
fieldId: props.fieldId,
|
||||
pageSize: 10,
|
||||
pageNo: pageNo.value,
|
||||
};
|
||||
let url = `/online/cgreport/api/getReportDictList`;
|
||||
await defHttp
|
||||
.get({ url: url, params }, { isTransformResponse: false })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
if (res.result && res.result.length > 0) {
|
||||
if (pageNo.value == 1) {
|
||||
selectOptions.value = [...res.result];
|
||||
} else {
|
||||
selectOptions.value.push(...res.result);
|
||||
}
|
||||
pageNo.value++;
|
||||
} else {
|
||||
if (pageNo.value == 1) {
|
||||
selectOptions.value = [];
|
||||
}
|
||||
isHasData = false;
|
||||
}
|
||||
} else {
|
||||
$message.warning(res.message);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
pageNo.value != 1 && pageNo.value--;
|
||||
});
|
||||
}
|
||||
|
||||
function handleChange(value) {
|
||||
emit('update:value', value);
|
||||
//点击clear按钮,重置下拉项
|
||||
if (!value || value == '') {
|
||||
resetOptions();
|
||||
}
|
||||
}
|
||||
function resetOptions() {
|
||||
selectOptions.value = [];
|
||||
// update-begin--author:liaozhiyang---date:20240717---for:【TV360X-1813】online报表查询支持滚动加载
|
||||
pageNo.value = 1;
|
||||
isHasData = true;
|
||||
searchKeyword = '';
|
||||
// update-end--author:liaozhiyang---date:20240717---for:【TV360X-1813】online报表查询支持滚动加载
|
||||
searchByKeyword();
|
||||
}
|
||||
/**
|
||||
* 2024-07-17
|
||||
* liaozhiyang
|
||||
* 【TV360X-1813】online报表查询支持滚动加载
|
||||
* */
|
||||
const handlePopupScroll = async (e) => {
|
||||
const { target } = e;
|
||||
const { scrollTop, scrollHeight, clientHeight } = target;
|
||||
if (!scrollLoading && isHasData && scrollTop + clientHeight >= scrollHeight - 10) {
|
||||
scrollLoading = true;
|
||||
searchByKeyword(searchKeyword).finally(() => {
|
||||
scrollLoading = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
return {
|
||||
selectOptions,
|
||||
handleSearch,
|
||||
handleChange,
|
||||
selected,
|
||||
handlePopupScroll,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div :id="tableName + '_form'">
|
||||
|
||||
<!-- 积木报表的打印按钮,只有配置了 reportUrl 才显示 -->
|
||||
<div v-if="!!formData.id && !!onlineExtConfigJson.reportPrintShow" style="text-align: right;position: absolute;top: 15px;right: 20px;z-index: 999">
|
||||
<PrinterOutlined title="打印" @click="onOpenReportPrint" style="font-size: 16px"/>
|
||||
</div>
|
||||
|
||||
<detail-form :schemas="detailFormSchemas" :data="formData" :span="formSpan"></detail-form>
|
||||
|
||||
<!-- 子表 -->
|
||||
<a-tabs v-if="themeTemplate !== ERP && hasSubTable && showSub" @change="onTabChange">
|
||||
<a-tab-pane v-for="(sub, index) in subTabInfo" :tab="sub.describe" :key="index + ''" :forceRender="true">
|
||||
<div :style="{ 'overflow-y': 'auto', 'overflow-x': 'hidden', 'max-height': subFormHeight + 'px' }" v-if="sub.relationType == 1">
|
||||
<!-- 子表-一对一 -->
|
||||
<online-sub-form-detail :key="subReloadKey" :table="sub.key" :form-template="formTemplate" :main-id="getSubTableForeignKeyValue(sub.foreignKey)" :properties="sub.properties"> </online-sub-form-detail>
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- 子表-一对多 -->
|
||||
<JVxeTable
|
||||
v-if="showStatus[sub.key]"
|
||||
:ref="refMap[sub.key]"
|
||||
keep-source
|
||||
:row-number="rowNumber"
|
||||
row-selection
|
||||
:height="subTableHeight"
|
||||
:disabled="true"
|
||||
:columns="sub.columns"
|
||||
:dataSource="subDataSource[sub.key]"
|
||||
:authPre="getSubTableAuthPre(sub.key)"
|
||||
/>
|
||||
<a-spin v-else :spinning="true"/>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<Loading :loading="loading" :absolute="false" />
|
||||
<slot name="bottom"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Loading } from '/@/components/Loading';
|
||||
import { getToken } from '/@/utils/auth';
|
||||
import { goJmReportViewPage } from '/@/utils';
|
||||
import { PrinterOutlined } from '@ant-design/icons-vue';
|
||||
import DetailForm from '../../extend/form/DetailForm.vue';
|
||||
import OnlineSubFormDetail from './OnlineSubFormDetail.vue';
|
||||
import { getDetailFormSchemas } from '../../hooks/auto/useAutoForm';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { ERP } from "../../util/constant";
|
||||
import { useAppInject } from '/@/hooks/web/useAppInject';
|
||||
import { useOnlineFormDetailContext } from '../../hooks/auto/useAutoFormDetail';
|
||||
import { useEnhance } from '../../hooks/auto/useEnhance';
|
||||
export default {
|
||||
name: 'OnlineFormDetail',
|
||||
components: {
|
||||
DetailForm,
|
||||
Loading,
|
||||
PrinterOutlined,
|
||||
OnlineSubFormDetail,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
formTemplate: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isTree: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
pidField: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
submitTip: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showSub:{
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
themeTemplate: {
|
||||
type: String,
|
||||
default: '',
|
||||
}
|
||||
},
|
||||
emits: ['success', 'rendered'],
|
||||
setup(props, { emit }) {
|
||||
console.log('onlineForm-setup》》');
|
||||
const { createMessage: $message } = useMessage();
|
||||
const { getIsMobile } = useAppInject();
|
||||
const tableName = ref('');
|
||||
const single = ref(true);
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
const tableType = ref(1);
|
||||
|
||||
const formData = ref<any>({});
|
||||
// update-begin-author:liaozhiyang---date:20240313---for:【QQYUN-9034】online弹窗一对一子表移动端内容高度设置不合理
|
||||
const subFormHeight = ref(getIsMobile.value ? 'auto' : 300);
|
||||
// update-end-author:liaozhiyang---date:20240313---for:【QQYUN-9034】online弹窗一对一子表移动端内容高度设置不合理
|
||||
const subReloadKey = ref(0);
|
||||
// 子表表格高度
|
||||
// 【VUEN-803】一对多子表固定340高度,修复自定义列组件被遮挡的问题
|
||||
const subTableHeight = ref(340);
|
||||
|
||||
const rowNumber = ref(getIsMobile.value ? false : true);
|
||||
|
||||
let detailData = {};
|
||||
// 字段展示状态
|
||||
const fieldDisplayStatus = reactive<any>({});
|
||||
|
||||
/**
|
||||
* online表单扩展配置
|
||||
*/
|
||||
const onlineExtConfigJson = reactive({
|
||||
reportPrintShow: 0,
|
||||
reportPrintUrl: '',
|
||||
joinQuery: 0,
|
||||
modelFullscreen: 0,
|
||||
modalMinWidth: '',
|
||||
});
|
||||
|
||||
const { detailFormSchemas, hasSubTable, subTabInfo, refMap, showStatus, subDataSource, createFormSchemas, formSpan } = getDetailFormSchemas(props);
|
||||
|
||||
/**
|
||||
* 处理扩展配置
|
||||
*/
|
||||
function handleExtConfigJson(jsonStr) {
|
||||
let extConfigJson = { reportPrintShow: 0, reportPrintUrl: '', joinQuery: 0, modelFullscreen: 1, modalMinWidth: '' };
|
||||
if (jsonStr) {
|
||||
extConfigJson = JSON.parse(jsonStr);
|
||||
}
|
||||
Object.keys(extConfigJson).map((k) => {
|
||||
onlineExtConfigJson[k] = extConfigJson[k];
|
||||
});
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240425---for:【issues/6139】online详情支持js增强loaded事件及设置值、获取值、隐藏功能
|
||||
const { onlineFormDetailContext, resetContext } = useOnlineFormDetailContext();
|
||||
let { EnhanceJS, initCgEnhanceJs } = useEnhance(onlineFormDetailContext, false);
|
||||
// update-end--author:liaozhiyang---date:20240425---for:【issues/6139】online详情支持js增强loaded事件及设置值、获取值、隐藏功能
|
||||
|
||||
// 渲染表单
|
||||
async function createRootProperties(data) {
|
||||
tableType.value = data.head.tableType;
|
||||
tableName.value = data.head.tableName;
|
||||
single.value = data.head.tableType == 1;
|
||||
handleExtConfigJson(data.head.extConfigJson);
|
||||
createFormSchemas(data.schema.properties);
|
||||
// update-begin--author:liaozhiyang---date:20240425---for:【issues/6139】online详情支持js增强loaded事件及设置值、获取值、隐藏功能
|
||||
EnhanceJS = initCgEnhanceJs(data.enhanceJs);
|
||||
// update-end--author:liaozhiyang---date:20240425---for:【issues/6139】online详情支持js增强loaded事件及设置值、获取值、隐藏功能
|
||||
emit('rendered', onlineExtConfigJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* status: 是否是修改页面
|
||||
* record: 列表页面的行数据
|
||||
* param: 树形列表添加子节点 传入的父级节点id
|
||||
* */
|
||||
async function show(_status, record) {
|
||||
console.log('进入表单详情》》form', record);
|
||||
// -update-begin--author:liaozhiyang---date:20251209---for:【QQYUN-13970】一对一子表编辑之后查看详情不会更新
|
||||
subReloadKey.value++;
|
||||
// -update-end--author:liaozhiyang---date:20251209---for:【QQYUN-13970】一对一子表编辑之后查看详情不会更新
|
||||
await edit(record);
|
||||
changeShowStatus(true);
|
||||
}
|
||||
|
||||
function getFormData(dataId) {
|
||||
let url = `/online/cgform/api/detail/${props.id}/${dataId}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
defHttp
|
||||
.get({ url }, { isTransformResponse: false })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
reject();
|
||||
$message.warning(res.message);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//update-begin-author:taoyan date:2023-2-13 for: QQYUN-4226【vue3】online 一对多子表 详情界面,序号错位了 点一下子表表格就正常了
|
||||
function changeShowStatus(flag){
|
||||
Object.keys(showStatus).map(k=>{
|
||||
showStatus[k] = flag;
|
||||
})
|
||||
}
|
||||
|
||||
function onTabChange(){
|
||||
changeShowStatus(false);
|
||||
setTimeout(()=>{
|
||||
changeShowStatus(true);
|
||||
}, 300);
|
||||
}
|
||||
//update-end-author:taoyan date:2023-2-13 for: QQYUN-4226【vue3】online 一对多子表 详情界面,序号错位了 点一下子表表格就正常了
|
||||
|
||||
async function edit(record) {
|
||||
let temp: any = await getFormData(record.id);
|
||||
// update-begin--author:liaozhiyang---date:20240425---for:【issues/6139】online详情支持js增强loaded事件及设置值、获取值、隐藏功能
|
||||
detailData = temp;
|
||||
// 每次打开有js增强设置隐藏的,都要先置成初始化。
|
||||
detailFormSchemas.value.filter((item) => item.hidden).forEach((item) => (item.hidden = false));
|
||||
Object.keys(fieldDisplayStatus).forEach(function (key) {
|
||||
delete fieldDisplayStatus[key];
|
||||
});
|
||||
handleEnhanceJS({ buttonCode: 'loaded' });
|
||||
// 表单赋值
|
||||
formData.value = { ...detailData };
|
||||
editSubVxeTableData(detailData);
|
||||
// update-end--author:liaozhiyang---date:20240425---for:【issues/6139】online详情支持js增强loaded事件及设置值、获取值、隐藏功能
|
||||
}
|
||||
|
||||
function editSubVxeTableData(record) {
|
||||
if (!record) {
|
||||
// 新增页面需要清空子表数据
|
||||
record = {};
|
||||
}
|
||||
let keys = Object.keys(subDataSource.value);
|
||||
if (keys && keys.length > 0) {
|
||||
let obj = {};
|
||||
for (let key of keys) {
|
||||
obj[key] = record[key] || [];
|
||||
}
|
||||
subDataSource.value = obj;
|
||||
}
|
||||
}
|
||||
|
||||
function getSubTableAuthPre(table) {
|
||||
return 'online_' + table + ':';
|
||||
}
|
||||
|
||||
//跳转至积木报表页面
|
||||
function onOpenReportPrint() {
|
||||
let url = onlineExtConfigJson.reportPrintUrl;
|
||||
let temp: any = formData.value;
|
||||
if (temp) {
|
||||
let id = temp.id;
|
||||
let token = getToken();
|
||||
goJmReportViewPage(url, id, token);
|
||||
}
|
||||
}
|
||||
|
||||
function getSubTableForeignKeyValue(key) {
|
||||
let temp = formData.value;
|
||||
console.log('getValueIgnoreCase(temp, key)', temp, key, getValueIgnoreCase(temp, key));
|
||||
return getValueIgnoreCase(temp, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* VUEN-1056 30、生成的一对多,编辑的时候,子表数据挂不上
|
||||
*/
|
||||
function getValueIgnoreCase(data, key) {
|
||||
if (data) {
|
||||
let temp = data[key];
|
||||
if (!temp && temp !== 0) {
|
||||
temp = data[key.toLowerCase()];
|
||||
if (!temp && temp !== 0) {
|
||||
temp = data[key.toUpperCase()];
|
||||
}
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240425---for:【issues/6139】online详情支持js增强loaded事件及设置值、获取值、隐藏功能
|
||||
function handleEnhanceJS({ buttonCode }) {
|
||||
if (EnhanceJS && EnhanceJS[buttonCode]) {
|
||||
EnhanceJS[buttonCode].call(onlineFormDetailContext, onlineFormDetailContext);
|
||||
}
|
||||
}
|
||||
watch(fieldDisplayStatus, (newValue) => {
|
||||
Object.entries(newValue).forEach(([key, value]) => {
|
||||
if (value == false) {
|
||||
const findItem = detailFormSchemas.value.find((item) => item.field === key);
|
||||
if (findItem) {
|
||||
findItem.hidden = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
const context = {
|
||||
setFieldsValue: (values) => {
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
detailData[key] = value;
|
||||
});
|
||||
},
|
||||
getFieldsValue: () => {
|
||||
return { ...detailData };
|
||||
},
|
||||
sh: fieldDisplayStatus,
|
||||
isUpdate: ref(false),
|
||||
isDetail: ref(true),
|
||||
};
|
||||
resetContext(context);
|
||||
// update-end--author:liaozhiyang---date:20240425---for:【issues/6139】online详情支持js增强loaded事件及设置值、获取值、隐藏功能
|
||||
|
||||
return {
|
||||
detailFormSchemas,
|
||||
formData,
|
||||
formSpan,
|
||||
|
||||
//主表
|
||||
tableName,
|
||||
loading,
|
||||
|
||||
//子表
|
||||
hasSubTable,
|
||||
subTabInfo,
|
||||
subFormHeight,
|
||||
subTableHeight,
|
||||
refMap,
|
||||
onTabChange,
|
||||
subReloadKey,
|
||||
|
||||
//一对多子表
|
||||
subDataSource,
|
||||
getSubTableAuthPre,
|
||||
|
||||
//父组件调用
|
||||
show,
|
||||
createRootProperties,
|
||||
|
||||
// 扩展配置
|
||||
onOpenReportPrint,
|
||||
onlineExtConfigJson,
|
||||
getSubTableForeignKeyValue,
|
||||
showStatus,
|
||||
ERP,
|
||||
rowNumber,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,911 @@
|
||||
<template>
|
||||
<div :id="tableName + '_form'" class="onlinePopFormWrap" :class="[`formTemplate_${formTemplate}`]">
|
||||
<BasicForm ref="onlineFormRef" @register="registerForm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { computed, ref, unref, nextTick, toRaw, reactive } from 'vue';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { SUBMIT_FLOW_KEY, VALIDATE_FAILED } from '../../types/onlineRender';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { pick } from 'lodash-es';
|
||||
import { useFormItems, getRefPromise, useOnlineFormContext } from '../../hooks/auto/useAutoForm';
|
||||
import { Loading } from '/@/components/Loading';
|
||||
import { useEnhance } from '../../hooks/auto/useEnhance';
|
||||
import OnlineSubForm from './OnlineSubForm.vue';
|
||||
import { loadFormFieldsDefVal } from '../../util/FieldDefVal';
|
||||
import { getToken } from '/@/utils/auth';
|
||||
import { goJmReportViewPage } from '/@/utils'
|
||||
import { PrinterOutlined, DiffOutlined, FormOutlined } from '@ant-design/icons-vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { Method } from 'axios';
|
||||
import { isObject } from '/@/utils/is';
|
||||
|
||||
const urlObject = {
|
||||
optPre: '/online/cgform/api/form/',
|
||||
urlButtonAction: '/online/cgform/api/doButton',
|
||||
};
|
||||
export default {
|
||||
name: 'OnlinePopForm',
|
||||
components: {
|
||||
BasicForm,
|
||||
Loading,
|
||||
OnlineSubForm,
|
||||
PrinterOutlined,
|
||||
DiffOutlined,
|
||||
FormOutlined
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
formTemplate: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isTree: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
pidField: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
submitTip:{
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
modalClass:{
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
//是否发送请求-即表单的保存/编辑请求,false则只将表单数据抛出去
|
||||
request: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否是vxeTable上方按钮点击打开的表单数据
|
||||
isVxeTableData: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['success', 'rendered', 'dataChange'],
|
||||
setup(props, { emit }) {
|
||||
console.log('onlineForm-setup》》');
|
||||
const { createMessage: $message } = useMessage();
|
||||
|
||||
const [registerVxeFormModal, { openModal:openVxeFormModal }] = useModal();
|
||||
const vxeTableId = ref('');
|
||||
// 表单ref
|
||||
const onlineFormRef = ref(null);
|
||||
const single = ref(true);
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
const tableType = ref(1);
|
||||
// 表单提交且提交流程
|
||||
const submitFlowFlag = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
|
||||
/**
|
||||
* online表单扩展配置
|
||||
*/
|
||||
const onlineExtConfigJson = reactive({
|
||||
reportPrintShow: 0,
|
||||
reportPrintUrl: '',
|
||||
joinQuery: 0,
|
||||
modelFullscreen: 0,
|
||||
modalMinWidth: '',
|
||||
});
|
||||
|
||||
const { onlineFormContext, resetContext } = useOnlineFormContext();
|
||||
const {
|
||||
formSchemas,
|
||||
defaultValueFields,
|
||||
changeDataIfArray2String,
|
||||
tableName,
|
||||
dbData,
|
||||
checkOnlyFieldValue,
|
||||
hasSubTable,
|
||||
subTabInfo,
|
||||
refMap,
|
||||
subDataSource,
|
||||
baseColProps,
|
||||
createFormSchemas,
|
||||
fieldDisplayStatus,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
labelWidth
|
||||
} = useFormItems(props, onlineFormRef);
|
||||
let { EnhanceJS, initCgEnhanceJs } = useEnhance(onlineFormContext, false);
|
||||
|
||||
//表单配置
|
||||
const [registerForm, { setProps, validate, resetFields, setFieldsValue, updateSchema, getFieldsValue, scrollToField }] = useForm({
|
||||
schemas: formSchemas,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: baseColProps,
|
||||
// update-begin--author:liaozhiyang---date:20240329---for:【QQYUN-7872】online表单label较长优化
|
||||
labelWidth,
|
||||
// update-end--author:liaozhiyang---date:20240329---for:【QQYUN-7872】online表单label较长优化
|
||||
// update-begin--author:liaozhiyang---date:20240105---for:【QQYUN-7499】多列风格富文本、markdown增加独占一行功能
|
||||
labelCol,
|
||||
wrapperCol
|
||||
// update-end--author:liaozhiyang---date:20240105---for:【QQYUN-7499】多列风格富文本、markdown增加独占一行功能
|
||||
});
|
||||
|
||||
// 表单禁用
|
||||
const onlineFormDisabled = ref(false);
|
||||
function handleFormDisabled() {
|
||||
let flag = props.disabled;
|
||||
onlineFormDisabled.value = flag;
|
||||
setProps({ disabled: flag });
|
||||
}
|
||||
|
||||
/**
|
||||
* status: 是否是修改页面
|
||||
* record: 列表页面的行数据
|
||||
* param: 树形列表添加子节点 传入的父级节点id
|
||||
* */
|
||||
async function show(status, record, param) {
|
||||
console.log('onlinepopform新增编辑进入表单》》form', record);
|
||||
await resetFields();
|
||||
dbData.value = '';
|
||||
let flag = unref(status);
|
||||
isUpdate.value = flag;
|
||||
if (flag) {
|
||||
// 编辑页面
|
||||
await edit(record);
|
||||
}
|
||||
await nextTick(() => {
|
||||
if (!flag && param) {
|
||||
//如果是新增页面 且 param传入有值 需要设置表单
|
||||
setFieldsValue(param);
|
||||
}
|
||||
handleDefaultValue();
|
||||
// 所有信息加载完毕 触发loaded事件
|
||||
handleCgButtonClick('js', 'loaded');
|
||||
//处理表单的禁用效果
|
||||
handleFormDisabled();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 当前表单默认值逻辑-进入新增页面触发
|
||||
*/
|
||||
function handleDefaultValue() {
|
||||
if (unref(isUpdate) === false) {
|
||||
let fieldProperties = toRaw(defaultValueFields[tableName.value]);
|
||||
loadFormFieldsDefVal(fieldProperties, (values) => {
|
||||
setFieldsValue(values);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function edit(record) {
|
||||
// 查询数据库
|
||||
let formData:any = await getFormData(record.id);
|
||||
if(!formData || Object.keys(formData).length==0){
|
||||
//没有查询出数据
|
||||
formData = {...toRaw(record)}
|
||||
}
|
||||
dbData.value = Object.assign({}, formData);
|
||||
//表单赋值
|
||||
let arr = realFormFieldNames.value;
|
||||
let values = pick(formData, ...arr);
|
||||
// 如果是vxetable上方按钮打开的表单,那么表单值以record为主,而不是数据库查询的数据,否则第一次修改后第二次打开表单,表单值和数据库一致但是和当前页面不一致
|
||||
if(props.isVxeTableData === true){
|
||||
values = Object.assign({},values, record)
|
||||
}
|
||||
await setFieldsValue(values);
|
||||
// editSubVxeTableData(formData);
|
||||
}
|
||||
|
||||
function editSubVxeTableData(record) {
|
||||
if (!record) {
|
||||
// 新增页面需要清空子表数据
|
||||
record = {};
|
||||
}
|
||||
let keys = Object.keys(subDataSource.value);
|
||||
if (keys && keys.length > 0) {
|
||||
let obj = {};
|
||||
for (let key of keys) {
|
||||
obj[key] = record[key] || [];
|
||||
}
|
||||
subDataSource.value = obj;
|
||||
}
|
||||
}
|
||||
|
||||
let realFormFieldNames = computed(() => {
|
||||
let arr = formSchemas.value;
|
||||
let names = [];
|
||||
for (let a of arr) {
|
||||
names.push(a.field);
|
||||
}
|
||||
return names;
|
||||
});
|
||||
|
||||
function getFormData(dataId) {
|
||||
let url = `${urlObject.optPre}${props.id}/${dataId}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
defHttp
|
||||
.get({ url }, { isTransformResponse: false })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
reject();
|
||||
$message.warning(res.message);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染表单
|
||||
async function createRootProperties(data) {
|
||||
tableType.value = data.head.tableType;
|
||||
tableName.value = data.head.tableName;
|
||||
single.value = data.head.tableType == 1;
|
||||
handleExtConfigJson(data.head.extConfigJson);
|
||||
|
||||
createFormSchemas(data.schema.properties, data.schema.required, checkOnlyFieldValue, onlineExtConfigJson);
|
||||
EnhanceJS = initCgEnhanceJs(data.enhanceJs);
|
||||
emit('rendered', onlineExtConfigJson);
|
||||
|
||||
//监听表单改变事件
|
||||
let formRefObject:any = await getRefPromise(onlineFormRef);
|
||||
formRefObject.$formValueChange = (field, value, changeFormData) => {
|
||||
onValuesChange(field, value);
|
||||
if(changeFormData){
|
||||
//如果存在其他表单控件的数据,直接设置该值
|
||||
setFieldsValue(changeFormData)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理扩展配置
|
||||
*/
|
||||
function handleExtConfigJson(jsonStr) {
|
||||
let extConfigJson = { reportPrintShow: 0, reportPrintUrl: '', joinQuery: 0, modelFullscreen: 1, modalMinWidth: '', formLabelLength: null };
|
||||
if (jsonStr) {
|
||||
extConfigJson = JSON.parse(jsonStr);
|
||||
}
|
||||
Object.keys(extConfigJson).map((k) => {
|
||||
onlineExtConfigJson[k] = extConfigJson[k];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function handleSubmit() {
|
||||
if (single.value === true) {
|
||||
handleSingleSubmit();
|
||||
} else {
|
||||
handleOne2ManySubmit();
|
||||
}
|
||||
}
|
||||
|
||||
function handleOne2ManySubmit() {
|
||||
validateAll().then((formData) => {
|
||||
handleApplyRequest(formData);
|
||||
});
|
||||
}
|
||||
|
||||
// 触发所有表单验证
|
||||
function validateAll() {
|
||||
let temp = {};
|
||||
return new Promise((resolve, reject) => {
|
||||
// 验证主表表单
|
||||
validate().then(
|
||||
(values) => resolve(values),
|
||||
({ errorFields }) => {
|
||||
reject({
|
||||
code: VALIDATE_FAILED,
|
||||
key: tableName.value,
|
||||
// 滚动到未通过校验的字段上
|
||||
scrollToField: () => errorFields[0] && scrollToField(errorFields[0].name, { behavior: 'smooth', block: 'center' }),
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
.then((result) => {
|
||||
Object.assign(temp, changeDataIfArray2String(result));
|
||||
return validateSubTableFields();
|
||||
})
|
||||
.then((allTableData) => {
|
||||
Object.assign(temp, allTableData);
|
||||
return Promise.resolve(temp);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e === VALIDATE_FAILED || e?.code === VALIDATE_FAILED) {
|
||||
$message.warning('校验未通过');
|
||||
if (e.key) {
|
||||
changeTab(e.key);
|
||||
if (e.scrollToField) {
|
||||
setTimeout(() => e.scrollToField(), 150)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(e);
|
||||
}
|
||||
return Promise.reject(null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换tab到出现校验错误的页面
|
||||
* */
|
||||
function changeTab(key) {
|
||||
let arr = subTabInfo.value;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (key == arr[i].key) {
|
||||
subActiveKey.value = i + '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证子表
|
||||
function validateSubTableFields() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let subData = {};
|
||||
try {
|
||||
let arr = subTabInfo.value;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let key = arr[i].key;
|
||||
let instance = refMap[key].value;
|
||||
// 兼容写法:如果取到的是一个数组类型,则取第一个元素
|
||||
if (instance instanceof Array) {
|
||||
instance = instance[0];
|
||||
}
|
||||
if (arr[i].relationType == 1) {
|
||||
try {
|
||||
let subFormData = await instance.getAll();
|
||||
subData[key] = [];
|
||||
subData[key].push(subFormData);
|
||||
} catch (e) {
|
||||
return reject({code: VALIDATE_FAILED, key, ...e});
|
||||
}
|
||||
} else {
|
||||
let errMap = await instance.fullValidateTable();
|
||||
if (errMap) {
|
||||
return reject({code: VALIDATE_FAILED, key});
|
||||
}
|
||||
subData[key] = instance.getTableData();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
resolve(subData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单提交-单表
|
||||
*/
|
||||
async function handleSingleSubmit() {
|
||||
try {
|
||||
let values = await validate();
|
||||
values = Object.assign({}, dbData.value, values);
|
||||
values = changeDataIfArray2String(values);
|
||||
loading.value = true;
|
||||
handleApplyRequest(values);
|
||||
} catch (error) {
|
||||
// update-begin--author:liaozhiyang---date:20240524---for:【TV360X-420】关联记录校验不通过的项在可视区外时点击保存没任何效果
|
||||
if (isObject(error)) {
|
||||
const errorFields = error.errorFields;
|
||||
if (errorFields?.length && errorFields[0].errors) {
|
||||
$message.warning(errorFields[0].errors[0]);
|
||||
scrollToField(errorFields[0].name, { behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
console.log(error);
|
||||
// update-end--author:liaozhiyang---date:20240524---for:【TV360X-420】关联记录校验不通过的项在可视区外时点击保存没任何效果
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
//提交数据前 先走一下自定义的JS校验
|
||||
function handleApplyRequest(formData) {
|
||||
customBeforeSubmit(context, formData)
|
||||
.then(() => {
|
||||
doApplyRequest(formData);
|
||||
})
|
||||
.catch((msg) => {
|
||||
$message.warning(msg);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function triggleChangeValues(values, id, target) {
|
||||
if (id && target) {
|
||||
if (target.setValues) {
|
||||
//一对一子表
|
||||
target.setValues(values);
|
||||
} else {
|
||||
//一对多子表
|
||||
target.setValues([
|
||||
{
|
||||
rowKey: id,
|
||||
values: values,
|
||||
},
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
//主表
|
||||
setFieldsValue(values);
|
||||
}
|
||||
}
|
||||
function triggleChangeValue(field, value) {
|
||||
let obj = {};
|
||||
obj[field] = value;
|
||||
setFieldsValue(obj);
|
||||
}
|
||||
|
||||
// 一对多子表Tab的Key,用于校验未通过时自动跳转
|
||||
const subActiveKey = ref('0');
|
||||
const subFormHeight = ref(300);
|
||||
// 子表表格高度
|
||||
// 【VUEN-803】一对多子表固定340高度,修复自定义列组件被遮挡的问题
|
||||
const subTableHeight = ref(340);
|
||||
|
||||
function getSubTableForeignKeyValue(key) {
|
||||
if (isUpdate.value === true) {
|
||||
let formData = dbData.value;
|
||||
return getValueIgnoreCase(formData, key);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* VUEN-1056 30、生成的一对多,编辑的时候,子表数据挂不上
|
||||
*/
|
||||
function getValueIgnoreCase(data, key) {
|
||||
if (data) {
|
||||
let temp = data[key];
|
||||
if (!temp && temp !== 0) {
|
||||
temp = data[key.toLowerCase()];
|
||||
if (!temp && temp !== 0) {
|
||||
temp = data[key.toUpperCase()];
|
||||
}
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
//处理一对一子表的表单改变事件
|
||||
function handleSubFormChange(valueObj, tableKey) {
|
||||
if (EnhanceJS && EnhanceJS[tableKey + '_onlChange']) {
|
||||
let tableChangeObj = EnhanceJS[tableKey + '_onlChange']();
|
||||
let columnKey = Object.keys(valueObj)[0];
|
||||
if (tableChangeObj[columnKey]) {
|
||||
let subRef = refMap[tableKey].value;
|
||||
if (subRef instanceof Array) {
|
||||
subRef = subRef[0];
|
||||
}
|
||||
let formEvent = subRef.getFormEvent();
|
||||
let event = {
|
||||
column: { key: columnKey },
|
||||
value: valueObj[columnKey],
|
||||
...formEvent,
|
||||
};
|
||||
tableChangeObj[columnKey].call(onlineFormContext, onlineFormContext, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//处理一对多子表的改变事件
|
||||
function handleValueChange(event, tableKey) {
|
||||
if (EnhanceJS && EnhanceJS[tableKey + '_onlChange']) {
|
||||
let tableChangeObj = EnhanceJS[tableKey + '_onlChange'](onlineFormContext);
|
||||
if (tableChangeObj[event.column.key]) {
|
||||
tableChangeObj[event.column.key].call(onlineFormContext, onlineFormContext, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 当行编辑新增完成后触发的事件
|
||||
function handleAdded(sub, event) {
|
||||
console.log('handleAdded', sub, event)
|
||||
//update-begin-author:taoyan date:2022-6-26 for: 控制台警告 这里直接调用函数 不触发事件了
|
||||
// event.target.emit('executeFillRule', event);
|
||||
//update-end-author:taoyan date:2022-6-26 for: 控制台警告 这里直接调用函数 不触发事件了
|
||||
}
|
||||
|
||||
function getSubTableAuthPre(table) {
|
||||
return 'online_' + table + ':';
|
||||
}
|
||||
|
||||
//监听表单改变事件
|
||||
async function onValuesChange(columnKey, value) {
|
||||
//console.log('columnKey-value', `${columnKey}-${value}`)
|
||||
// 将老数据和新数据比较 如果不同 往外抛出改变事件
|
||||
let oldFormData = dbData.value;
|
||||
if(oldFormData[columnKey]!=value){
|
||||
emit('dataChange', columnKey);
|
||||
}
|
||||
|
||||
if (!EnhanceJS || !EnhanceJS['onlChange']) {
|
||||
return false;
|
||||
}
|
||||
if (!columnKey) {
|
||||
return false;
|
||||
}
|
||||
//let tableChangeObj = EnhanceJS["onlChange"].call(onlineFormContext);
|
||||
let tableChangeObj = EnhanceJS['onlChange']();
|
||||
if (tableChangeObj[columnKey]) {
|
||||
let formData = await getFieldsValue();
|
||||
let event = {
|
||||
row: formData,
|
||||
column: { key: columnKey },
|
||||
value: value,
|
||||
};
|
||||
tableChangeObj[columnKey].call(onlineFormContext, onlineFormContext, event);
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义按钮 增强触发事件
|
||||
function handleCgButtonClick(optType, buttonCode) {
|
||||
if ('js' == optType) {
|
||||
if (EnhanceJS && EnhanceJS[buttonCode]) {
|
||||
EnhanceJS[buttonCode].call(onlineFormContext, onlineFormContext);
|
||||
}
|
||||
} else if ('action' == optType) {
|
||||
let formData = dbData.value;
|
||||
let params = {
|
||||
formId: props.id,
|
||||
buttonCode: buttonCode,
|
||||
dataId: formData.id,
|
||||
uiFormData: Object.assign({}, formData),
|
||||
};
|
||||
//console.log("自定义按钮请求后台参数:",params)
|
||||
defHttp
|
||||
.post(
|
||||
{
|
||||
url: `${urlObject.urlButtonAction}`,
|
||||
params,
|
||||
},
|
||||
{ isTransformResponse: false }
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
$message.success('处理完成!');
|
||||
} else {
|
||||
$message.warning('处理失败!');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------增强-------------------------
|
||||
|
||||
/**
|
||||
* 清除子表数据
|
||||
* @param tbname
|
||||
*/
|
||||
function clearSubRows(tbname) {
|
||||
let instance = refMap[tbname].value;
|
||||
let rows = [...instance.getNewDataWithId(), ...subDataSource.value[tbname]];
|
||||
if (!rows || rows.length == 0) {
|
||||
return false;
|
||||
}
|
||||
let ids = [];
|
||||
for (let i of rows) {
|
||||
ids.push(i.id);
|
||||
}
|
||||
instance.removeRowsById(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加子表数据
|
||||
* @param tbname
|
||||
* @param rows 可以是数组也可以是对象
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function addSubRows(tbname, rows) {
|
||||
if (!rows) {
|
||||
return false;
|
||||
}
|
||||
let instance = refMap[tbname].value;
|
||||
if (typeof rows == 'object') {
|
||||
instance.addRows(rows, true);
|
||||
} else {
|
||||
$message.error('添加子表数据,参数不识别!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 先删除后添加
|
||||
* @param tbname
|
||||
* @param rows
|
||||
*/
|
||||
function clearThenAddRows(tbname, rows) {
|
||||
clearSubRows(tbname);
|
||||
addSubRows(tbname, rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改下拉框的下拉选项
|
||||
* @param field
|
||||
* @param options
|
||||
*/
|
||||
function changeOptions(field, options) {
|
||||
if (!options && options.length <= 0) {
|
||||
options = [];
|
||||
}
|
||||
options.map((item) => {
|
||||
if (!item.hasOwnProperty('label')) {
|
||||
item['label'] = item.text;
|
||||
}
|
||||
});
|
||||
updateSchema({
|
||||
field,
|
||||
componentProps: {
|
||||
options,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单提交前事件
|
||||
* @param that
|
||||
* @param formData
|
||||
* @returns {Promise<void>|*}
|
||||
*/
|
||||
function customBeforeSubmit(that, formData) {
|
||||
if (EnhanceJS && EnhanceJS['beforeSubmit']) {
|
||||
return EnhanceJS['beforeSubmit'](that, formData);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理自定义弹框 字段的显示隐藏
|
||||
* @param show
|
||||
* @param hide
|
||||
*/
|
||||
function handleCustomFormSh(show, hide) {
|
||||
let plain = toRaw(fieldDisplayStatus);
|
||||
if (show && show.length > 0) {
|
||||
Object.keys(plain).map((k) => {
|
||||
if (!k.endsWith('_load') && show.indexOf(k) < 0) {
|
||||
fieldDisplayStatus[k] = false;
|
||||
}
|
||||
});
|
||||
} else if (hide && hide.length > 0) {
|
||||
Object.keys(plain).map((k) => {
|
||||
if (hide.indexOf(k) >= 0) {
|
||||
fieldDisplayStatus[k] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCustomFormEdit(record) {
|
||||
console.log('自定义弹窗打开online表单》》form', record);
|
||||
await resetFields();
|
||||
dbData.value = '';
|
||||
isUpdate.value = true;
|
||||
// 编辑数据
|
||||
await edit(record);
|
||||
await nextTick(() => {
|
||||
// 所有信息加载完毕 触发loaded事件
|
||||
handleCgButtonClick('js', 'loaded');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* VUEN-1036
|
||||
* 获取子表的实例对象 可以直接调用子表的方法
|
||||
*/
|
||||
function getSubTableInstance(tableName) {
|
||||
let instance = refMap[tableName].value;
|
||||
if (instance instanceof Array) {
|
||||
instance = instance[0];
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
//跳转至积木报表页面
|
||||
function onOpenReportPrint(){
|
||||
let url = onlineExtConfigJson.reportPrintUrl;
|
||||
let id = dbData.value.id;
|
||||
let token = getToken();
|
||||
goJmReportViewPage(url, id, token)
|
||||
}
|
||||
|
||||
//----
|
||||
function openSubFormModalForAdd(sub){
|
||||
console.log(sub)
|
||||
vxeTableId.value = sub.id;
|
||||
openVxeFormModal(true, )
|
||||
}
|
||||
|
||||
function openSubFormModalForEdit(sub){
|
||||
console.log(sub)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据
|
||||
* @param formData
|
||||
*/
|
||||
function doApplyRequest(formData) {
|
||||
// 数组没有元素直接置空
|
||||
Object.keys(formData).map((key) => {
|
||||
if (Array.isArray(formData[key])) {
|
||||
if (formData[key].length == 0) {
|
||||
formData[key] = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('提交pop表单数据》》》form:', formData);
|
||||
if(props.request == false){
|
||||
emit('success', formData);
|
||||
}else{
|
||||
let url = `${urlObject.optPre}${props.id}?tabletype=${tableType.value}`;
|
||||
|
||||
console.log('提交pop表单url》》》url:', url);
|
||||
// 如果需要提交流程 需要额外设置一个参数
|
||||
if (submitFlowFlag.value === true) {
|
||||
formData[SUBMIT_FLOW_KEY] = 1;
|
||||
}
|
||||
let method:Method = isUpdate.value === true ? 'put' : 'post';
|
||||
defHttp.request({ url, method, params: formData }, { isTransformResponse: false })
|
||||
.then((res) => {
|
||||
//console.log('表单提交完成', res)
|
||||
if (res.success) {
|
||||
if (res.result) {
|
||||
//formData[SUBMIT_FLOW_ID] = res.result;
|
||||
if(!formData.id){
|
||||
formData['id'] = res.result;
|
||||
}
|
||||
}
|
||||
//刷新列表
|
||||
emit('success', formData);
|
||||
dbData.value = formData;
|
||||
isUpdate.value = true;
|
||||
$message.success('操作成功!')
|
||||
// 工单申请提交的表单也会走这个逻辑,保存成功不需要提示信息
|
||||
} else {
|
||||
$message.warning(res.message);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 数据恢复到 dbdata
|
||||
*/
|
||||
async function recoverFormData(){
|
||||
let record = dbData.value;
|
||||
let arr = realFormFieldNames.value;
|
||||
let values = pick(record, ...arr);
|
||||
if(record){
|
||||
await setFieldsValue(values);
|
||||
}else{
|
||||
let temp:any = {}
|
||||
for(let key of arr){
|
||||
temp[key] = ''
|
||||
}
|
||||
await setFieldsValue(temp);
|
||||
}
|
||||
}
|
||||
|
||||
let context = {
|
||||
tableName,
|
||||
loading,
|
||||
subActiveKey,
|
||||
onlineFormRef,
|
||||
getFieldsValue,
|
||||
setFieldsValue,
|
||||
submitFlowFlag,
|
||||
subFormHeight,
|
||||
subTableHeight,
|
||||
refMap,
|
||||
triggleChangeValues,
|
||||
triggleChangeValue,
|
||||
sh: fieldDisplayStatus,
|
||||
clearSubRows,
|
||||
addSubRows,
|
||||
clearThenAddRows,
|
||||
changeOptions,
|
||||
isUpdate,
|
||||
getSubTableInstance,
|
||||
};
|
||||
resetContext(context);
|
||||
|
||||
return {
|
||||
//主表
|
||||
tableName,
|
||||
onlineFormRef,
|
||||
registerForm,
|
||||
loading,
|
||||
|
||||
//子表
|
||||
subActiveKey,
|
||||
hasSubTable,
|
||||
subTabInfo,
|
||||
refMap,
|
||||
|
||||
//一对一子表
|
||||
subFormHeight,
|
||||
getSubTableForeignKeyValue,
|
||||
isUpdate,
|
||||
handleSubFormChange,
|
||||
|
||||
//一对多子表
|
||||
subTableHeight,
|
||||
onlineFormDisabled,
|
||||
subDataSource,
|
||||
getSubTableAuthPre,
|
||||
handleAdded,
|
||||
handleValueChange,
|
||||
openSubFormModalForAdd,
|
||||
openSubFormModalForEdit,
|
||||
registerVxeFormModal,
|
||||
vxeTableId,
|
||||
|
||||
//父组件调用
|
||||
show,
|
||||
createRootProperties,
|
||||
handleSubmit,
|
||||
sh: fieldDisplayStatus,
|
||||
handleCgButtonClick,
|
||||
handleCustomFormSh,
|
||||
handleCustomFormEdit,
|
||||
//跳转
|
||||
dbData,
|
||||
onOpenReportPrint,
|
||||
onlineExtConfigJson,
|
||||
recoverFormData
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.onlinePopFormWrap {
|
||||
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-7632】 label栅格改成labelwidth固宽
|
||||
padding: 20px 1.5% 0 1.5%;
|
||||
// update-begin--author:liaozhiyang---date:20240506---for:【QQYUN-9229】间隔调整
|
||||
&.formTemplate_1 {
|
||||
> form {
|
||||
padding-left: 5%;
|
||||
padding-right: 5%;
|
||||
}
|
||||
}
|
||||
&.formTemplate_2 {
|
||||
> form {
|
||||
padding-left: 1%;
|
||||
padding-right: 1%;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240506---for:【QQYUN-9229】间隔调整
|
||||
:deep(.ant-form) {
|
||||
> .ant-row {
|
||||
> .ant-col {
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-7632】 label栅格改成labelwidth固宽
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<BasicModal @register="registerModal" :width="popModalFixedWidth" :dialogStyle="{top: '70px'}" :bodyStyle="popBodyStyle" :title="modalTitle" wrapClassName="jeecg-online-pop-list-modal">
|
||||
<template #footer>
|
||||
<div style="display: inline-block;width: calc(100% - 140px);text-align: left;">
|
||||
<a-button v-if="addAuth" style="border-radius: 50px" type="primary" @click="handleAdd"><PlusOutlined/>新增记录</a-button>
|
||||
</div>
|
||||
<a-button key="back" @click="handleCancel">关闭</a-button>
|
||||
<a-button :disabled="submitDisabled" key="submit" type="primary" @click="handleSubmit" :loading="submitLoading">确定</a-button>
|
||||
</template>
|
||||
|
||||
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
|
||||
<!-- update-begin-author:taoyan date:2023-7-11 for: issues/4992 online表单开发 字段控件类型是关联记录 新增的时候选择列表可以添加查询么 -->
|
||||
<template #tableTitle>
|
||||
<a-input-search v-model:value="searchText" @search="onSearch" placeholder="请输入关键词,按回车搜索" style="width: 240px" />
|
||||
</template>
|
||||
<!-- update-end-author:taoyan date:2023-7-11 for: issues/4992 online表单开发 字段控件类型是关联记录 新增的时候选择列表可以添加查询么 -->
|
||||
|
||||
<!--操作栏-->
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getTableAction(record)">
|
||||
</TableAction>
|
||||
</template>
|
||||
|
||||
<template #fileSlot="{ text }">
|
||||
<span v-if="!text" style="font-size: 12px; font-style: italic">无文件</span>
|
||||
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download" size="small" @click="downloadRowFile(text)"> 下载 </a-button>
|
||||
</template>
|
||||
|
||||
<template #imgSlot="{ text }">
|
||||
<span v-if="!text" style="font-size: 12px; font-style: italic">无图片</span>
|
||||
<img v-else :src="getImgView(text)" alt="图片不存在" class="online-cell-image" @click="viewOnlineCellImage(text)" />
|
||||
</template>
|
||||
|
||||
<template #htmlSlot="{ text }">
|
||||
<div v-html="text"></div>
|
||||
</template>
|
||||
|
||||
<template #pcaSlot="{ text, column }">
|
||||
<div :title="getPcaText(text, column)">{{ getPcaText(text, column) }}</div>
|
||||
</template>
|
||||
|
||||
<template #dateSlot="{ text, column }">
|
||||
<span>{{ getFormatDate(text, column) }}</span>
|
||||
</template>
|
||||
|
||||
</BasicTable>
|
||||
</BasicModal>
|
||||
|
||||
<!-- 弹窗到另外一张表单用-可编辑表单 -->
|
||||
<online-pop-modal :id="id" @register="registerPopModal" @success="handleDataSave" topTip></online-pop-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch, ref, toRaw, computed } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useTableColumns } from '../../hooks/auto/useTableColumns';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import OnlinePopModal from './OnlinePopModal.vue';
|
||||
import { useFixedHeightModal } from '../../hooks/auto/useAutoModal';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OnlinePopListModal',
|
||||
props: {
|
||||
/**可以是表名 可以是ID*/
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
multi:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
addAuth:{
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BasicModal,
|
||||
BasicTable,
|
||||
TableAction,
|
||||
PlusOutlined,
|
||||
OnlinePopModal
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const { createMessage: $message } = useMessage();
|
||||
// 弹窗高度控制
|
||||
const { popModalFixedWidth, resetBodyStyle, popBodyStyle } = useFixedHeightModal();
|
||||
const searchText = ref('');
|
||||
const modalWidth = ref(800);
|
||||
//useModalInner
|
||||
const [registerModal, {closeModal}] = useModalInner((data) => {
|
||||
searchText.value = '';
|
||||
// update-begin--author:liaozhiyang---date:20240517---for:【TV360X-43】修复关联记录可以添加重复数据
|
||||
selectedRowKeys.value = data.selectedRowKeys;
|
||||
selectedRows.value = data.selectedRows;
|
||||
// update-end--author:liaozhiyang---date:20240517---for:【TV360X-43】修复关联记录可以添加重复数据
|
||||
setPagination({current:1})
|
||||
reload();
|
||||
resetBodyStyle();
|
||||
});
|
||||
|
||||
// 用于 online表单中 弹出别的表单
|
||||
const [registerPopModal, { openModal: openPopModal }] = useModal();
|
||||
|
||||
function handleCancel() {
|
||||
closeModal();
|
||||
}
|
||||
const submitDisabled = computed(()=>{
|
||||
const arr = selectedRowKeys.value;
|
||||
if(arr && arr.length>0){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
const submitLoading = ref(false);
|
||||
function handleSubmit(){
|
||||
submitLoading.value = true;
|
||||
let arr = toRaw(selectedRows.value);
|
||||
if(arr && arr.length>0){
|
||||
emit('success', arr)
|
||||
closeModal();
|
||||
}
|
||||
setTimeout(()=>{
|
||||
submitLoading.value = false
|
||||
}, 200);
|
||||
}
|
||||
|
||||
//---------------------列表------------------------
|
||||
function queryTableData(params){
|
||||
const url = '/online/cgform/api/getData/'+props.id;
|
||||
return defHttp.get({ url, params });
|
||||
}
|
||||
|
||||
function list(params){
|
||||
params['column'] = 'id';
|
||||
return new Promise(async (resolve, _reject) => {
|
||||
const aa = await queryTableData(params)
|
||||
resolve(aa);
|
||||
})
|
||||
}
|
||||
|
||||
const onlineTableContext = {
|
||||
isPopList: true,
|
||||
reloadTable(){
|
||||
console.log('reloadTable')
|
||||
},
|
||||
isTree(){
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const extConfigJson = ref<any>({});
|
||||
|
||||
// 处理 BasicTable 的配置
|
||||
const {
|
||||
columns,
|
||||
downloadRowFile,
|
||||
getImgView,
|
||||
getPcaText,
|
||||
getFormatDate,
|
||||
handleColumnResult,
|
||||
hrefComponent,
|
||||
viewOnlineCellImage,
|
||||
} = useTableColumns(onlineTableContext, extConfigJson);
|
||||
|
||||
|
||||
/**
|
||||
* 查询table列信息 及其他配置
|
||||
*/
|
||||
function getColumnList() {
|
||||
const url = '/online/cgform/api/getColumns/'+props.id;
|
||||
return new Promise((resolve, reject) => {
|
||||
defHttp.get({url}, { isTransformResponse: false }).then((res) => {
|
||||
if (res.success) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
$message.warning(res.message);
|
||||
reject();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const modalTitle = ref('')
|
||||
watch(()=>props.id, async ()=>{
|
||||
let columnResult:any = await getColumnList();
|
||||
handleColumnResult(columnResult);
|
||||
modalTitle.value = columnResult.description;
|
||||
}, {immediate: true})
|
||||
|
||||
const { tableContext } = useListPage({
|
||||
designScope: 'process-design',
|
||||
pagination: true,
|
||||
tableProps: {
|
||||
title: '',
|
||||
api: list,
|
||||
clickToRowSelect: true,
|
||||
columns: columns,
|
||||
showTableSetting: false,
|
||||
immediate:false,
|
||||
//showIndexColumn: true,
|
||||
canResize: false,
|
||||
showActionColumn: false,
|
||||
actionColumn: {
|
||||
dataIndex: 'action',
|
||||
slots: { customRender: 'action' },
|
||||
},
|
||||
useSearchForm: false,
|
||||
beforeFetch: (params) => {
|
||||
return addQueryParams(params);
|
||||
},
|
||||
},
|
||||
});
|
||||
const [registerTable, { reload, setPagination }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
|
||||
watch(()=>props.multi, (val)=>{
|
||||
if(val==true){
|
||||
rowSelection.type = 'checkbox'
|
||||
}else{
|
||||
rowSelection.type = 'radio'
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
/**
|
||||
* 操作栏
|
||||
*/
|
||||
function getTableAction(record) {
|
||||
return [
|
||||
{
|
||||
label: '编辑',
|
||||
onClick: handleUpdate.bind(null, record),
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function handleUpdate(record){
|
||||
console.log('handleUpdate', record)
|
||||
}
|
||||
|
||||
function onSearch(){
|
||||
reload();
|
||||
}
|
||||
const eqConditonTypes = ['int', 'double', 'Date', 'Datetime', 'BigDecimal']
|
||||
function addQueryParams(params){
|
||||
let text = searchText.value;
|
||||
if(!text){
|
||||
params['superQueryMatchType'] = 'or';
|
||||
params['superQueryParams'] = ''
|
||||
return params;
|
||||
}
|
||||
let arr = columns.value;
|
||||
let conditions:any[] = []
|
||||
if(arr && arr.length>0){
|
||||
for(let item of arr){
|
||||
if(item.dbType){
|
||||
if(item.dbType == 'string'){
|
||||
conditions.push({field: item.dataIndex,type:item.dbType.toLowerCase(), rule: 'like', val: text})
|
||||
}else if(item.dbType == 'Date'){
|
||||
if(text.length=='2020-10-10'.length){
|
||||
conditions.push({field: item.dataIndex,type:item.dbType.toLowerCase(), rule: 'eq', val: text})
|
||||
}
|
||||
}else if(item.dbType == 'Datetime'){
|
||||
if(text.length=='2020-10-10 10:10:10'.length){
|
||||
conditions.push({field: item.dataIndex,type:item.dbType.toLowerCase(), rule: 'eq', val: text})
|
||||
}
|
||||
}else if(eqConditonTypes.indexOf(item.dbType)){
|
||||
conditions.push({field: item.dataIndex, type:item.dbType.toLowerCase(), rule: 'eq', val: text})
|
||||
}else{
|
||||
//text blob不做处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
params['superQueryMatchType'] = 'or';
|
||||
params['superQueryParams'] = encodeURI(JSON.stringify(conditions));
|
||||
return params;
|
||||
}
|
||||
|
||||
function handleAdd(){
|
||||
openPopModal(true, {})
|
||||
}
|
||||
|
||||
// modal数据新增完成 直接关闭list,将新增的数据带回表单
|
||||
function handleDataSave(data){
|
||||
console.log('handleDateSave' ,data)
|
||||
// update-begin--author:liaozhiyang---date:20250429---for:【issues/8163】关联记录新增丢失
|
||||
let arr = [data, ...selectedRows.value];
|
||||
// update-end--author:liaozhiyang---date:20250429---for:【issues/8163】关联记录新增丢失
|
||||
emit('success', arr);
|
||||
closeModal();
|
||||
//reload();
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
modalWidth,
|
||||
handleCancel,
|
||||
submitDisabled,
|
||||
submitLoading,
|
||||
handleSubmit,
|
||||
|
||||
registerTable,
|
||||
getTableAction,
|
||||
searchText,
|
||||
onSearch,
|
||||
|
||||
downloadRowFile,
|
||||
getImgView,
|
||||
getPcaText,
|
||||
getFormatDate,
|
||||
hrefComponent,
|
||||
viewOnlineCellImage,
|
||||
rowSelection,
|
||||
modalTitle,
|
||||
|
||||
registerPopModal,
|
||||
handleAdd,
|
||||
reload,
|
||||
|
||||
popModalFixedWidth,
|
||||
popBodyStyle,
|
||||
handleDataSave
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.online-cell-image {
|
||||
max-height: 30px;
|
||||
max-width: 50px;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<BasicModal :width="popModalFixedWidth" :dialogStyle="{top: '70px'}" :bodyStyle="popBodyStyle" v-bind="$attrs" :footer="modalFooter" cancelText="关闭" @register="registerModal" wrapClassName="jeecg-online-pop-modal" @ok="handleSubmit">
|
||||
<template #title>
|
||||
{{title}}
|
||||
<j-modal-tip v-if="showTopTip" :visible="topTipVisible" @save="handleSaveData" @cancel="handleRecover"></j-modal-tip>
|
||||
</template>
|
||||
|
||||
<online-pop-form
|
||||
ref="onlineFormCompRef"
|
||||
:id="id"
|
||||
:disabled="disableSubmit"
|
||||
:form-template="formTemplate"
|
||||
:isTree="isTreeForm"
|
||||
:pidField="pidFieldName"
|
||||
:request="request"
|
||||
:isVxeTableData="isVxeTableData"
|
||||
@rendered="renderSuccess"
|
||||
@success="handleSuccess"
|
||||
@data-change="handleDataChange"
|
||||
modal-class="jeecg-online-pop-modal"
|
||||
>
|
||||
</online-pop-form>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch, watchEffect, ref, computed, h } from 'vue';
|
||||
import { BasicModal } from '/@/components/Modal';
|
||||
import OnlinePopForm from './OnlinePopForm.vue';
|
||||
import { useAutoModal } from '../../hooks/auto/useAutoModal';
|
||||
import JModalTip from '../../extend/linkTable/JModalTip.vue'
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OnlinePopModal',
|
||||
props: {
|
||||
/**可以是表名 可以是ID*/
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/*展示字段名*/
|
||||
showFields:{
|
||||
type: Array,
|
||||
default: ()=>[],
|
||||
},
|
||||
/*隐藏字段名*/
|
||||
hideFields:{
|
||||
type: Array,
|
||||
default: ()=>[],
|
||||
},
|
||||
topTip:{
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
request:{
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
saveClose:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否是vxeTable上方按钮点击打开的表单数据
|
||||
isVxeTableData:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
formTableType:{
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// -update-begin--author:liaozhiyang---date:20240613---for:【TV360X-1000】流程一对多走流程的接口
|
||||
// 有taskId即是流程
|
||||
taskId: {
|
||||
type: String,
|
||||
},
|
||||
tableName: {
|
||||
type: String,
|
||||
},
|
||||
// -update-end--author:liaozhiyang---date:20240613---for:【TV360X-1000】流程一对多走流程的接口
|
||||
},
|
||||
components: {
|
||||
BasicModal,
|
||||
OnlinePopForm,
|
||||
JModalTip,
|
||||
Button
|
||||
},
|
||||
emits: ['success', 'register', 'formConfig'],
|
||||
setup(props, { emit }) {
|
||||
console.log('进入表单弹框》》》》modal');
|
||||
|
||||
const {
|
||||
title,
|
||||
registerModal,
|
||||
cgButtonList,
|
||||
handleCgButtonClick,
|
||||
disableSubmit,
|
||||
handleSubmit,
|
||||
submitLoading,
|
||||
handleCancel,
|
||||
handleFormConfig,
|
||||
onlineFormCompRef,
|
||||
formTemplate,
|
||||
isTreeForm,
|
||||
pidFieldName,
|
||||
renderSuccess,
|
||||
formRendered,
|
||||
handleSuccess,
|
||||
topTipVisible,
|
||||
successThenClose,
|
||||
isUpdate,
|
||||
popBodyStyle,
|
||||
popModalFixedWidth,
|
||||
getFormStatus
|
||||
} = useAutoModal(false, { emit });
|
||||
|
||||
// 监听id变化 表单重新渲染
|
||||
watch(() => props.id, renderFormItems, { immediate: true });
|
||||
async function renderFormItems() {
|
||||
formRendered.value = false;
|
||||
if (!props.id) {
|
||||
return;
|
||||
}
|
||||
console.log('重新渲染表单》》》》modal');
|
||||
|
||||
//update-begin-author:taoyan date:2023-4-10 for: issues/4655 online在线表单(一对多),对子表记录进行新增或编辑时,无法获取到表单信息 #4655
|
||||
let params = {}
|
||||
if(props.formTableType){
|
||||
params['tabletype'] = props.formTableType
|
||||
}
|
||||
// -update-begin--author:liaozhiyang---date:20240613---for:【TV360X-1000】流程一对多走流程的接口
|
||||
if (props.taskId) {
|
||||
await handleFormConfig(props.id, params,null,props.taskId, props.tableName );
|
||||
} else {
|
||||
await handleFormConfig(props.id, params);
|
||||
}
|
||||
// -update-end--author:liaozhiyang---date:20240613---for:【TV360X-1000】流程一对多走流程的接口
|
||||
//update-end-author:taoyan date:2023-4-10 for: issues/4655 online在线表单(一对多),对子表记录进行新增或编辑时,无法获取到表单信息 #4655
|
||||
|
||||
}
|
||||
|
||||
// 上方保存按钮触发
|
||||
function handleSaveData() {
|
||||
//如果props的saveClose没有设置为true则弹窗不会关闭
|
||||
if(props.saveClose === false){
|
||||
successThenClose.value = false;
|
||||
}
|
||||
handleSubmit();
|
||||
}
|
||||
|
||||
// 上方取消按钮触发
|
||||
function handleRecover(){
|
||||
topTipVisible.value = false;
|
||||
onlineFormCompRef.value.recoverFormData()
|
||||
}
|
||||
|
||||
// 表单数据改变触发modal事件
|
||||
function handleDataChange(){
|
||||
topTipVisible.value = true;
|
||||
}
|
||||
|
||||
// 只有编辑页面才需要显示顶部保存按钮
|
||||
const showTopTip = computed(()=>{
|
||||
// update-begin--author:liaozhiyang---date:20250318---for:【issues/7930】表格列表中支持关联记录配置是否只读
|
||||
if (disableSubmit.value) {
|
||||
return false;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20250318---for:【issues/7930】表格列表中支持关联记录配置是否只读
|
||||
if(!isUpdate.value){
|
||||
return false;
|
||||
}
|
||||
return props.topTip
|
||||
});
|
||||
|
||||
// 编辑页面没有底部按钮
|
||||
const modalFooter = computed(()=>{
|
||||
if(isUpdate.value==true){
|
||||
return null;
|
||||
}else{
|
||||
let flag = submitLoading.value;
|
||||
const defaultFooter:any[] = [
|
||||
h(Button, { type: 'primary', loading: flag, onClick: handleSubmit },()=>'确定'),
|
||||
h(Button, { onClick:handleCancel },()=>'关闭')
|
||||
];
|
||||
return defaultFooter
|
||||
}
|
||||
});
|
||||
|
||||
const that = {
|
||||
title,
|
||||
topTipVisible,
|
||||
handleSaveData,
|
||||
handleRecover,
|
||||
onlineFormCompRef,
|
||||
renderSuccess,
|
||||
registerModal,
|
||||
handleSubmit,
|
||||
handleSuccess,
|
||||
handleCancel,
|
||||
formTemplate,
|
||||
disableSubmit,
|
||||
cgButtonList,
|
||||
handleCgButtonClick,
|
||||
isTreeForm,
|
||||
pidFieldName,
|
||||
submitLoading,
|
||||
handleDataChange,
|
||||
isUpdate,
|
||||
showTopTip,
|
||||
modalFooter,
|
||||
popBodyStyle,
|
||||
popModalFixedWidth,
|
||||
getFormStatus
|
||||
};
|
||||
|
||||
return that;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,863 @@
|
||||
<template>
|
||||
<div class="jeecg-basic-table-form-container online-query-form p-0" v-if="formSchemas && formSchemas.length > 0">
|
||||
<BasicForm ref="onlineQueryFormRef" @register="registerForm">
|
||||
<!-- 范围查询:日期 -->
|
||||
<template #groupDate="{ model, field, schema }">
|
||||
<!-- update-begin--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换 -->
|
||||
<!-- <a-date-picker
|
||||
:showTime="false"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="开始日期"
|
||||
v-model:value="model[field + '_begin']"
|
||||
style="width: calc(50% - 15px);min-width: 100px;"
|
||||
v-bind="schema.componentProps"
|
||||
></a-date-picker>
|
||||
<span class="group-query-string">~</span>
|
||||
<a-form-item-rest>
|
||||
<a-date-picker
|
||||
:showTime="false"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="结束日期"
|
||||
v-model:value="model[field + '_end']"
|
||||
style="width: calc(50% - 15px);min-width: 100px;"
|
||||
v-bind="schema.componentProps"
|
||||
></a-date-picker>
|
||||
</a-form-item-rest> -->
|
||||
<a-range-picker :style="{ width: '100%' }" v-model:value="model[field]" v-bind="schema.componentProps" :placeholder="getGroupDatePlaceholder(schema.componentProps)" valueFormat="YYYY-MM-DD"/>
|
||||
<!-- update-end--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换 -->
|
||||
</template>
|
||||
|
||||
<!-- 范围查询:时间 -->
|
||||
<template #groupDatetime="{ model, field }">
|
||||
<!-- update-begin--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换 -->
|
||||
<!-- <a-date-picker
|
||||
:showTime="true"
|
||||
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="开始时间"
|
||||
v-model:value="model[field + '_begin']"
|
||||
style="min-width: 100px;width: calc(50% - 15px);"
|
||||
></a-date-picker>
|
||||
<span class="group-query-string">~</span>
|
||||
<a-form-item-rest>
|
||||
<a-date-picker
|
||||
:showTime="true"
|
||||
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="结束时间"
|
||||
v-model:value="model[field + '_end']"
|
||||
style="min-width: 100px;width: calc(50% - 15px);"
|
||||
></a-date-picker>
|
||||
</a-form-item-rest> -->
|
||||
<a-range-picker :style="{ width: '100%' }" v-model:value="model[field]" :show-time="true" valueFormat="YYYY-MM-DD HH:mm:ss"/>
|
||||
<!-- update-end--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换 -->
|
||||
</template>
|
||||
|
||||
<!-- update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9348】增加online查询区域时间范围查询功能 -->
|
||||
<!-- 范围查询:时间 -->
|
||||
<template #groupTime="{ model, field }">
|
||||
<!-- update-begin--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换 -->
|
||||
<!-- <a-time-picker
|
||||
placeholder="开始时间"
|
||||
value-format="HH:mm:ss"
|
||||
v-model:value="model[field + '_begin']"
|
||||
style="min-width: 100px;width: calc(50% - 15px);"
|
||||
></a-time-picker>
|
||||
<span class="group-query-string">~</span>
|
||||
<a-form-item-rest>
|
||||
<a-time-picker
|
||||
placeholder="结束时间"
|
||||
value-format="HH:mm:ss"
|
||||
v-model:value="model[field + '_end']"
|
||||
style="min-width: 100px;width: calc(50% - 15px);"
|
||||
></a-time-picker>
|
||||
</a-form-item-rest> -->
|
||||
<a-time-range-picker :style="{ width: '100%' }" v-model:value="model[field]" value-format="HH:mm:ss" />
|
||||
<!-- update-end--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换 -->
|
||||
</template>
|
||||
<!-- update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9348】增加online查询区域时间范围查询功能 -->
|
||||
|
||||
<!-- 范围查询:数值 -->
|
||||
<template #groupNumber="{ model, field, schema }">
|
||||
<!-- update-begin--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换 -->
|
||||
<!-- <a-input-number placeholder="开始值" v-model:value="model[field + '_begin']" style="width: calc(50% - 15px)"></a-input-number>
|
||||
<span class="group-query-string">~</span>
|
||||
<a-form-item-rest>
|
||||
<a-input-number placeholder="结束值" v-model:value="model[field + '_end']" style="width: calc(50% - 15px)"></a-input-number>
|
||||
</a-form-item-rest> -->
|
||||
<JRangeNumber v-model:value="model[field]" v-bind="schema.componentProps" />
|
||||
<!-- update-end--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换 -->
|
||||
</template>
|
||||
|
||||
<!-- 查询/重置按钮-->
|
||||
<template #formFooter>
|
||||
<a-col :md="6" :sm="8">
|
||||
<span style="float: left; overflow: hidden; margin-left: 10px" class="table-page-search-submitButtons">
|
||||
<a-button
|
||||
v-if="queryBtnCfg.enabled"
|
||||
type="primary"
|
||||
:preIcon="queryBtnCfg.buttonIcon"
|
||||
@click="doSearch"
|
||||
>
|
||||
<span>{{ queryBtnCfg.buttonName }}</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="resetBtnCfg.enabled"
|
||||
type="primary"
|
||||
:preIcon="resetBtnCfg.buttonIcon"
|
||||
style="margin-left: 8px"
|
||||
@click="resetSearch"
|
||||
>
|
||||
<span>{{ resetBtnCfg.buttonName }}</span>
|
||||
</a-button>
|
||||
<a v-if="toggleButtonShow" @click="toggleSearchStatus = !toggleSearchStatus" style="margin-left: 8px">
|
||||
{{ toggleSearchStatus ? '收起' : '展开' }}
|
||||
<a-icon :type="toggleSearchStatus ? 'up' : 'down'" />
|
||||
</a>
|
||||
</span>
|
||||
</a-col>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { watch, ref, reactive, toRaw, isProxy } from 'vue';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import FormSchemaFactory from './factory/FormSchemaFactory';
|
||||
import IFormSchema from './factory/IFormSchema';
|
||||
import { handleLinkDown, getFieldIndex, getRefPromise, LINK_DOWN } from '../../hooks/auto/useAutoForm';
|
||||
import { ONL_QUERY_LABEL_COL, ONL_QUERY_WRAPPER_COL, FORM_VIEW_TO_QUERY_VIEW } from '../../types/onlineRender';
|
||||
import { loadOneFieldDefVal } from '../../util/FieldDefVal';
|
||||
import { useExtendComponent } from '../../hooks/auto/useExtendComponent';
|
||||
import { LABELLENGTH } from '../../util/constant';
|
||||
import dayjs from 'dayjs';
|
||||
import JRangeNumber from '/@/components/Form/src/jeecg/components/JRangeNumber.vue'
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
|
||||
export default {
|
||||
name: 'OnlineQueryForm',
|
||||
components: {
|
||||
BasicForm,
|
||||
JRangeNumber,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
queryBtnCfg: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
enabled: true,
|
||||
buttonName: '查询',
|
||||
buttonIcon: 'ant-design:search',
|
||||
}
|
||||
}
|
||||
},
|
||||
resetBtnCfg: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
enabled: true,
|
||||
buttonName: '重置',
|
||||
buttonIcon: 'ant-design:reload',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
emits: ['search', 'loaded'],
|
||||
setup(props, { emit }) {
|
||||
// 获取查询条件请求地址
|
||||
const LOAD_URL = '/online/cgform/api/getQueryInfoVue3/';
|
||||
// 表单ref
|
||||
const onlineQueryFormRef = ref(null);
|
||||
// 表单渲染用到的配置
|
||||
const formSchemas = ref([]);
|
||||
// 表单栅格 VUEN-2493【优化】online默认查询条件太宽了,参考online报表
|
||||
const baseColProps = ref({ xs:24, sm: 24, md: 12, lg:6, xl:6 });
|
||||
// 切换字段显示隐藏按钮是否显示
|
||||
const toggleButtonShow = ref(false);
|
||||
// 是否显示所有查询字段
|
||||
const toggleSearchStatus = ref(false);
|
||||
// 查询条件
|
||||
const queryParams = ref({});
|
||||
// 需要隐藏的字段
|
||||
const hideList = ref([]);
|
||||
const { createMessage: $message } = useMessage();
|
||||
const { linkTableCard2Select } = useExtendComponent();
|
||||
const formLabelWidth = ref(80);
|
||||
/**
|
||||
* 默认值分三种,param > cache > config
|
||||
* 1.表单配置-config
|
||||
* 2.路由缓存-cache
|
||||
* 3.地址栏参数-param
|
||||
* 当表单id发生改变,config改变,列表页传入cache,param;监听status change然后重置表单的值
|
||||
*/
|
||||
const defaultValues = reactive({
|
||||
config: {},
|
||||
cache: {},
|
||||
param: {},
|
||||
status: false,
|
||||
});
|
||||
|
||||
const debouncedCustomSetFieldsValue = useDebounceFn(customSetFieldsValue, 500);
|
||||
|
||||
/**
|
||||
* 监听cacheFormValues
|
||||
*/
|
||||
watch(
|
||||
() => defaultValues.status,
|
||||
async (val) => {
|
||||
console.log('-------------defaultValues发生改变,需要重置表单---------------');
|
||||
const { config, cache, param } = toRaw(defaultValues);
|
||||
let rawValues = Object.assign({}, config, cache, param);
|
||||
//update-begin---author:wangshuai---date:2025-10-11---for:【issues/8790】online 表单重大 bug,影响配置了查询 的所有表单---
|
||||
await debouncedCustomSetFieldsValue(rawValues);
|
||||
//update-end---author:wangshuai---date:2025-10-11---for:【issues/8790】online 表单重大 bug,影响配置了查询 的所有表单---
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 设置默认值
|
||||
* @param values
|
||||
*/
|
||||
async function initDefaultValues(cache, param) {
|
||||
defaultValues.cache = { ...cache };
|
||||
defaultValues.param = { ...param };
|
||||
defaultValues.status = !defaultValues.status;
|
||||
}
|
||||
|
||||
const clearObj = (obj) => {
|
||||
Object.keys(obj).map((key) => {
|
||||
delete obj[key];
|
||||
});
|
||||
};
|
||||
|
||||
// 监听
|
||||
watch(
|
||||
() => props.id,
|
||||
(val) => {
|
||||
if (val) {
|
||||
resetForm();
|
||||
} else {
|
||||
formSchemas.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取表单配置
|
||||
*/
|
||||
async function initSchemas(formProperties) {
|
||||
let arr = [];
|
||||
let configValue = {};
|
||||
let keys = Object.keys(formProperties);
|
||||
let setLabelLength = -1;
|
||||
for (let key of keys) {
|
||||
const item = formProperties[key];
|
||||
|
||||
//update-begin-author:taoyan date:2023-7-19 for:QQYUN-5783 配置的数据字典参数,问题是在查询的部门选择组件那儿,表单定义不能更改传给后端org_code。看后端代码,给组件value传的也是id,这儿需要调整。
|
||||
if(key === 'sys_org_code'){
|
||||
if(!item.fieldExtendJson){
|
||||
item.fieldExtendJson = '{"store":"orgCode"}'
|
||||
}
|
||||
}
|
||||
//update-end-author:taoyan date:2023-7-19 for:QQYUN-5783 配置的数据字典参数,问题是在查询的部门选择组件那儿,表单定义不能更改传给后端org_code。看后端代码,给组件value传的也是id,这儿需要调整。
|
||||
|
||||
let view = item.view;
|
||||
// update-begin--author:liaozhiyang---date:20240611---for:【TV360X-461】字段类型是string,控件是text,则默认模糊查询
|
||||
item.originView = item.view;
|
||||
// update-end--author:liaozhiyang---date:20240611---for:【TV360X-461】字段类型是string,控件是text,则默认模糊查询
|
||||
if (FORM_VIEW_TO_QUERY_VIEW[view]) {
|
||||
item.view = FORM_VIEW_TO_QUERY_VIEW[view];
|
||||
}
|
||||
await loadOneFieldDefVal(key, item, configValue);
|
||||
if (item.mode == 'group' && ('date' == view || 'datetime' == view || 'number' == view || 'time' == view )) {
|
||||
// 范围查询-日期,时间,数值
|
||||
let temp = FormSchemaFactory.createSlotFormSchema(key, item);
|
||||
arr.push(temp);
|
||||
} else {
|
||||
if (item.view === LINK_DOWN) {
|
||||
let array = handleLinkDown(item, key);
|
||||
for (let linkDownItem of array) {
|
||||
let temp = FormSchemaFactory.createFormSchema(linkDownItem.key, linkDownItem);
|
||||
let tempIndex = getFieldIndex(arr, linkDownItem.key);
|
||||
if (tempIndex == -1) {
|
||||
arr.push(temp);
|
||||
} else {
|
||||
arr[tempIndex] = temp;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let tempIndex = getFieldIndex(arr, key);
|
||||
if (tempIndex == -1) {
|
||||
let temp = FormSchemaFactory.createFormSchema(key, item);
|
||||
arr.push(temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20231205---for:【QQYUN-7140】online label默认显示6个
|
||||
let fieldExtendJson = item.fieldExtendJson;
|
||||
if (fieldExtendJson) {
|
||||
fieldExtendJson = JSON.parse(fieldExtendJson);
|
||||
if (fieldExtendJson.labelLength) {
|
||||
console.log(key, fieldExtendJson.labelLength);
|
||||
if (setLabelLength > -1) {
|
||||
// 取配置中设置的最大值,所有label的长度应一致,否则多行会对不齐
|
||||
setLabelLength = fieldExtendJson.labelLength > setLabelLength ? fieldExtendJson.labelLength : setLabelLength;
|
||||
} else {
|
||||
setLabelLength = fieldExtendJson.labelLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231205---for:【QQYUN-7140】online label默认显示6个
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20231205---for:【QQYUN-7140】online label默认显示6个
|
||||
// 配置中没有设置,读取默认的长度
|
||||
if (setLabelLength == -1) {
|
||||
setLabelLength = LABELLENGTH;
|
||||
} else {
|
||||
// update-begin--author:liaozhiyang---date:20240517---for:【TV360X-98】label展示的文字必须和labelLength配置一致
|
||||
arr.forEach(item=>{
|
||||
item.labelLength = setLabelLength;
|
||||
})
|
||||
// update-end--author:liaozhiyang---date:20240517---for:【TV360X-98】label展示的文字必须和labelLength配置一致
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231205---for:【QQYUN-7140】online label默认显示6个
|
||||
arr.sort(function (a, b) {
|
||||
return a.order - b.order;
|
||||
});
|
||||
let schemaArray = [];
|
||||
if (arr.length > 2) {
|
||||
toggleButtonShow.value = true;
|
||||
}
|
||||
let hideFieldName = [];
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let item = arr[i];
|
||||
item.setFormRef(onlineQueryFormRef);
|
||||
item.noChange();
|
||||
item.asSearchForm();
|
||||
if (i > 1) {
|
||||
hideFieldName.push(item.field);
|
||||
item.isHidden();
|
||||
}
|
||||
//update-begin-author:taoyan date:2022-10-24 for: VUEN-2493【优化】online默认查询条件太宽了,参考online报表
|
||||
let tempSchema = item.getFormItemSchema();
|
||||
if(item.slot == 'groupDatetime'){
|
||||
// update-begin--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换
|
||||
//如果是时间类型 重新设定colprops (小于3个时才重置,否则多行会导致对不齐)
|
||||
arr.length <= 3 && (tempSchema['colProps'] = { xs:24, sm: 24, md: 12, lg:8, xl:8 })
|
||||
// update-end--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240522---for:【TV360X-250】查询区域把Switch开关组件改成select组件
|
||||
if (tempSchema.component === 'JSwitch') {
|
||||
const componentProps = tempSchema.componentProps ?? {};
|
||||
tempSchema.componentProps = { ...componentProps, query: true };
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240522---for:【TV360X-250】查询区域把Switch开关组件改成select组件
|
||||
linkTableCard2Select(tempSchema);
|
||||
// update-begin--author:liaozhiyang---date:20240530---for:【TV360X-389】普通查询关联记录去掉编辑按钮
|
||||
if (tempSchema.component === 'LinkTableSelect') {
|
||||
let componentProps = tempSchema.componentProps ?? {};
|
||||
tempSchema.componentProps = { ...componentProps, editBtnShow: false };
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240530---for:【TV360X-389】普通查询关联记录去掉编辑按钮
|
||||
// update-begin--author:liaozhiyang---date:20240614---for:【TV360X-1231】查询区域有的下拉组件显示不全
|
||||
const compProps = tempSchema.componentProps ?? {};
|
||||
if (!compProps.getPopupContainer) {
|
||||
tempSchema.componentProps = { ...compProps, getPopupContainer: () => document.body };
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240614---for:【TV360X-1231】查询区域有的下拉组件显示不全
|
||||
// update-begin--author:liaozhiyang---date:20240725---for:【TV360X-1857】online查询增加模糊查询
|
||||
const fieldData = formProperties[tempSchema.field] ?? {};
|
||||
// 【TV360X-1966】页面属性控件配置了文本框且字段类型是string,个性化查询配置了用户组件,用户组件不生效
|
||||
if (fieldData.mode == 'like' && fieldData.view === 'text' && fieldData.originView === 'text') {
|
||||
tempSchema.component = 'JInput';
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240725---for:【TV360X-1857】online查询增加模糊查询
|
||||
schemaArray.push(tempSchema);
|
||||
//update-end-author:taoyan date:2022-10-24 for: VUEN-2493【优化】online默认查询条件太宽了,参考online报表
|
||||
}
|
||||
hideList.value = hideFieldName;
|
||||
formSchemas.value = schemaArray;
|
||||
//设置表单默认值
|
||||
defaultValues.config = { ...configValue };
|
||||
defaultValues.status = !defaultValues.status;
|
||||
// update-begin--author:liaozhiyang---date:20231204---for:【QQYUN-7140】online label默认显示6个
|
||||
setTimeout(() => {
|
||||
// 14是文字size,24是间隙
|
||||
const w = setLabelLength * 14 + setLabelLength + 24;
|
||||
formLabelWidth.value = w;
|
||||
}, 0);
|
||||
// update-end--author:liaozhiyang---date:20231204---for:【QQYUN-7140】online label默认显示6个
|
||||
}
|
||||
/**
|
||||
* 2024-05-31
|
||||
* liaozhiyang
|
||||
* 【TV360X-415】个性化查询支持年、月、周、季度.
|
||||
* 解析特定view(组件)字段的值,把view字段值为date_year、date_month、date_week、date_quarter
|
||||
* 改成date,并组装fieldExtendJson
|
||||
*/
|
||||
const analysisComponent = (res) => {
|
||||
const properties = res.properties;
|
||||
if (properties) {
|
||||
Object.entries(properties).forEach(([key, value]) => {
|
||||
const data = value;
|
||||
if (['date_year', 'date_month', 'date_week', 'date_quarter'].includes(data.view)) {
|
||||
const fieldExtendJson = data.fieldExtendJson ? JSON.parse(data.fieldExtendJson) : {};
|
||||
fieldExtendJson.picker = data.view.split('_')[1];
|
||||
data.fieldExtendJson = JSON.stringify(fieldExtendJson);
|
||||
data.view = 'date';
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
async function resetForm() {
|
||||
let json = await loadQueryInfo();
|
||||
// update-begin--author:liaozhiyang---date:20240531---for:【TV360X-415】个性化查询支持年、月、周、季度
|
||||
analysisComponent(json);
|
||||
// update-end--author:liaozhiyang---date:20240318---for:【TV360X-415】个性化查询支持年、月、周、季度
|
||||
// update-begin--author:liaozhiyang---date:20240524---for:【TV360X-516】高级查询过滤掉不支持查询的组件
|
||||
// filterComponent(json);
|
||||
// update-end--author:liaozhiyang---date:20240524---for:【TV360X-516】高级查询过滤掉不支持查询的组件
|
||||
// 获取所有字段配置 通过事件回传给高级查询组件
|
||||
let allFields = getAllFields(json);
|
||||
emit('loaded', json);
|
||||
// 获取查询条件表单页面配置
|
||||
let { formProperties, hasField } = getQueryFormProperties(allFields, json);
|
||||
if (hasField == false) {
|
||||
formSchemas.value = [];
|
||||
return;
|
||||
}
|
||||
// 获取表单配置formSchemas
|
||||
await initSchemas(formProperties);
|
||||
}
|
||||
/**
|
||||
* 2024-05-24
|
||||
* liaozhiyang
|
||||
* 过滤掉不支持查询的组件(图片、文件、密码、关联记录、联动)
|
||||
*/
|
||||
const filterComponent = (data) => {
|
||||
const { properties = {} } = data;
|
||||
Object.entries(properties).forEach(([field, value]) => {
|
||||
if (value.view === 'table') {
|
||||
filterComponent(value);
|
||||
}
|
||||
if (['image', 'password', 'file', 'link_table', 'link_down'].includes(value.view)) {
|
||||
delete properties[field];
|
||||
}
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 设置表单的值
|
||||
*/
|
||||
async function customSetFieldsValue(rawValues) {
|
||||
await getRefPromise(onlineQueryFormRef);
|
||||
console.log('rawValues', rawValues);
|
||||
// update-begin--author:liaozhiyang---date:20240618---for:online普通查询默认值范围查询不好使
|
||||
const values = transformGroupDefValus(rawValues);
|
||||
await setFieldsValue(values);
|
||||
// update-end--author:liaozhiyang---date:20240618---for:online普通查询默认值范围查询不好使
|
||||
if (Object.keys(values).length > 0) {
|
||||
doSearch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转化
|
||||
*/
|
||||
function getQueryFormProperties(allFields, json) {
|
||||
const { searchFieldList, joinQuery, table } = json;
|
||||
let hasField = false;
|
||||
let formProperties = {};
|
||||
if (allFields) {
|
||||
Object.keys(allFields).map((field) => {
|
||||
if (searchFieldList.indexOf(field) >= 0) {
|
||||
//只找需要查询的字段
|
||||
if (joinQuery == true) {
|
||||
//判断是不是联合查询
|
||||
if (field.indexOf('@') < 0) {
|
||||
//没有@说明是主表字段, 手动拼接上@
|
||||
formProperties[table + '@' + field] = allFields[field];
|
||||
hasField = true;
|
||||
} else {
|
||||
//有@说明是子表字段,直接获取
|
||||
formProperties[field] = allFields[field];
|
||||
hasField = true;
|
||||
}
|
||||
} else {
|
||||
// 不是联合查询 只查主表字段
|
||||
if (field.indexOf('@') < 0) {
|
||||
formProperties[field] = allFields[field];
|
||||
hasField = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
formProperties,
|
||||
hasField,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取查询条件表单配置
|
||||
* json结构:
|
||||
* 主表字段1:{配置1}
|
||||
* 主表字段2:{配置2}
|
||||
* 子表名@子表字段1:{子表字段配置1}
|
||||
* 子表名@子表字段2:{子表字段配置2}
|
||||
*/
|
||||
function getAllFields(json) {
|
||||
// 获取所有配置 查询字段 是否联合查询
|
||||
const { properties, searchFieldList, joinQuery, table } = json;
|
||||
let allFields = {};
|
||||
let order = 1;
|
||||
let hasField = false;
|
||||
Object.keys(properties).map((field) => {
|
||||
let item = properties[field];
|
||||
if (item.view == 'table') {
|
||||
// 子表字段
|
||||
// 联合查询开启才需要子表字段作为查询条件
|
||||
let subProps = item['properties'];
|
||||
let subTableOrder = order * 100;
|
||||
Object.keys(subProps).map((subField) => {
|
||||
let subItem = subProps[subField];
|
||||
// 保证排序统一
|
||||
subItem['order'] = subTableOrder + Number(subItem['order']);
|
||||
let subFieldKey = field + '@' + subField;
|
||||
allFields[subFieldKey] = subItem;
|
||||
});
|
||||
order++;
|
||||
} else {
|
||||
// 主表字段
|
||||
item['order'] = Number(item['order']);
|
||||
allFields[field] = item;
|
||||
}
|
||||
});
|
||||
return allFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询返回数据格式, 需要经过getQueryFormProperties转成表单配置
|
||||
* json结构:
|
||||
* table(表名)
|
||||
* title(表描述)
|
||||
* properties(表字段)
|
||||
* field1
|
||||
* field2
|
||||
* fieldxxx
|
||||
* sub-table1(子表名1)
|
||||
* title(子表描述1)
|
||||
* properties(子表字段)
|
||||
* sub-table2(子表名2)
|
||||
* title(子表描述2)
|
||||
* properties(子表字段)
|
||||
*/
|
||||
function loadQueryInfo() {
|
||||
let url = `${LOAD_URL}${props.id}`;
|
||||
return new Promise((resolve) => {
|
||||
defHttp
|
||||
.get({ url }, { isTransformResponse: false })
|
||||
.then((res) => {
|
||||
// console.log("-online列表查询条件获取配置", res);
|
||||
if (res.success) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
resolve(false);
|
||||
$message.warning(res.message);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
$message.warning('获取查询条件失败!');
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//表单配置
|
||||
const [registerForm, { resetFields, setFieldsValue, updateSchema, getFieldsValue }] = useForm({
|
||||
name: 'online-query-form',
|
||||
schemas: formSchemas,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: baseColProps,
|
||||
autoSubmitOnEnter: true,
|
||||
labelWidth: formLabelWidth,
|
||||
wrapperCol: null,
|
||||
submitFunc() {
|
||||
//update-begin---author:wangshuai---date:2025-10-11---for:【issues/8790】online 表单重大 bug,影响配置了查询 的所有表单---
|
||||
//doSearch();
|
||||
//update-end---author:wangshuai---date:2025-10-11---for:【issues/8790】online 表单重大 bug,影响配置了查询 的所有表单---
|
||||
},
|
||||
/* labelCol: ONL_QUERY_LABEL_COL,
|
||||
wrapperCol: ONL_QUERY_WRAPPER_COL*/
|
||||
});
|
||||
|
||||
/**
|
||||
* 执行查询
|
||||
*/
|
||||
function doSearch() {
|
||||
let formValues = getFieldsValue();
|
||||
// update-begin--author:liaozhiyang---date:20240517---for:【TV360X-28】年,年月,周查询出结果不准
|
||||
transformDateValus(formValues);
|
||||
// update-end--author:liaozhiyang---date:20240517---for:【TV360X-28】年,年月,周查询出结果不准
|
||||
// update-begin--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换
|
||||
transformGroupValus(formValues);
|
||||
// update-end--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换
|
||||
// 还需要把地址栏参数添加进去
|
||||
let data = Object.assign({}, toRaw(defaultValues.param), changeDataIfArray2String(formValues));
|
||||
emit('search', data, true);
|
||||
}
|
||||
/**
|
||||
* 2024-06-18
|
||||
* liaozhiyang
|
||||
* online普通查询默认值范围查询不好使
|
||||
* */
|
||||
const transformGroupDefValus = (obj) => {
|
||||
const values = { ...obj };
|
||||
const groupSchemas = formSchemas.value.filter((item) => ['groupTime', 'groupDatetime', 'groupNumber', 'groupDate'].includes(item.slot));
|
||||
if (groupSchemas.length) {
|
||||
Object.keys(values).forEach((filed) => {
|
||||
let key;
|
||||
const findItem = groupSchemas.find((item) => {
|
||||
if (item.field === filed) {
|
||||
key = filed;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (findItem) {
|
||||
const value = values[key];
|
||||
if (typeof value === 'string') {
|
||||
const arr = value.split(',');
|
||||
values[key] = [...arr];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return values;
|
||||
};
|
||||
|
||||
/**
|
||||
* 2024-05-20
|
||||
* liaozhiyang
|
||||
* 【TV360X-213】把groupDatetime,groupTime,groupDate,groupNumber等范围字段分割成两个字段
|
||||
*/
|
||||
const transformGroupValus = (values) => {
|
||||
if (values) {
|
||||
const groupSchemas = formSchemas.value.filter((item) => ['groupTime', 'groupDatetime', 'groupDate', 'groupNumber'].includes(item.slot));
|
||||
if (groupSchemas.length) {
|
||||
Object.keys(values).forEach((filed) => {
|
||||
let key;
|
||||
const findItem = groupSchemas.find((item) => {
|
||||
if (item.field === filed) {
|
||||
key = filed;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (findItem) {
|
||||
const value = values[key];
|
||||
if (typeof value === 'string') {
|
||||
const arr = value.split(',');
|
||||
values[`${key}_begin`] = arr[0];
|
||||
values[`${key}_end`] = arr[1];
|
||||
delete values[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 2024-05-20
|
||||
* liaozhiyang
|
||||
* 把年,年月,周等时间重置到当前格式的第一天,因为存的时候也是第一天 (【TV360X-180】兼容时间范围)
|
||||
*/
|
||||
const transformDateValus = (values) => {
|
||||
const dateSchemas = formSchemas.value.filter((item) => item.componentProps?.picker && item.componentProps.picker != 'default');
|
||||
if (dateSchemas.length) {
|
||||
Object.keys(values).forEach((filed) => {
|
||||
let key;
|
||||
const findItem = dateSchemas.find((item) => {
|
||||
if (item.field === filed || `${item.field}_begin` === filed || `${item.field}_end` === filed) {
|
||||
key = filed;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (findItem) {
|
||||
const value = values[key];
|
||||
if (value) {
|
||||
// update-begin--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换
|
||||
const auto = (value, key, isEnd) => {
|
||||
const picker = findItem.componentProps.picker;
|
||||
if (picker === 'year') {
|
||||
if (isEnd) {
|
||||
values[key] = dayjs(value).endOf('year').format('YYYY-MM-DD');
|
||||
} else {
|
||||
values[key] = dayjs(value).startOf('year').format('YYYY-MM-DD');
|
||||
}
|
||||
} else if (picker === 'month') {
|
||||
if (isEnd) {
|
||||
values[key] = dayjs(value).endOf('month').format('YYYY-MM-DD');
|
||||
} else {
|
||||
values[key] = dayjs(value).startOf('month').format('YYYY-MM-DD');
|
||||
}
|
||||
} else if (picker === 'week') {
|
||||
if (isEnd) {
|
||||
values[key] = dayjs(value).endOf('week').format('YYYY-MM-DD');
|
||||
} else {
|
||||
values[key] = dayjs(value).startOf('week').format('YYYY-MM-DD');
|
||||
}
|
||||
} else if (picker === 'quarter') {
|
||||
if (isEnd) {
|
||||
values[key] = dayjs(value).endOf('quarter').format('YYYY-MM-DD');
|
||||
} else {
|
||||
values[key] = dayjs(value).startOf('quarter').format('YYYY-MM-DD');
|
||||
}
|
||||
}
|
||||
};
|
||||
if (findItem?.slot === 'groupDate') {
|
||||
const arr = value.split(',');
|
||||
auto(arr[0], `${key}_begin`, false);
|
||||
auto(arr[1], `${key}_end`, true);
|
||||
delete values[key];
|
||||
} else {
|
||||
auto(value, key, false);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240530---for:【TV360X-213】普通查询日期数值组件更换
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置是将 查询控件的值 重置到默认值
|
||||
* 2024-05-23
|
||||
* liaozhiyang
|
||||
* 【TV360X-124】提供clearSearch方法回到初始状态
|
||||
*/
|
||||
async function clearSearch() {
|
||||
await resetFields();
|
||||
const { config, param } = toRaw(defaultValues);
|
||||
let rawValues = Object.assign({}, config, param);
|
||||
if (Object.keys(rawValues).length > 0) {
|
||||
await setFieldsValue(rawValues);
|
||||
}
|
||||
return rawValues;
|
||||
}
|
||||
|
||||
async function resetSearch() {
|
||||
const rawValues = await clearSearch();
|
||||
emit('search', rawValues, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 有些数据是数组格式的 强转成字符串
|
||||
*/
|
||||
function changeDataIfArray2String(data) {
|
||||
Object.keys(data).map((k) => {
|
||||
if (data[k]) {
|
||||
if (data[k] instanceof Array) {
|
||||
data[k] = data[k].join(',');
|
||||
}
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => toggleSearchStatus.value,
|
||||
(status) => {
|
||||
let names = hideList.value;
|
||||
if (names && names.length > 0) {
|
||||
let arr = [];
|
||||
for (let name of names) {
|
||||
arr.push({
|
||||
field: name,
|
||||
show: status,
|
||||
});
|
||||
}
|
||||
updateSchema(arr);
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
);
|
||||
/**
|
||||
* 2024-05-30
|
||||
* liaozhiyang
|
||||
* 【TV360X-392】日期placeholder修改
|
||||
* */
|
||||
const getGroupDatePlaceholder = (data) => {
|
||||
let result = ['开始日期', '结束日期'];
|
||||
console.log(data);
|
||||
if (data?.picker) {
|
||||
switch (data?.picker) {
|
||||
case 'year':
|
||||
result = ['开始年份', '结束年份'];
|
||||
break;
|
||||
case 'month':
|
||||
result = ['开始月份', '结束月份'];
|
||||
break;
|
||||
case 'week':
|
||||
result = ['开始周', '结束周'];
|
||||
break;
|
||||
case 'quarter':
|
||||
result = ['开始季度', '结束季度'];
|
||||
break;
|
||||
default:
|
||||
result = ['开始日期', '结束日期'];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return {
|
||||
onlineQueryFormRef,
|
||||
registerForm,
|
||||
initDefaultValues,
|
||||
toggleButtonShow,
|
||||
toggleSearchStatus,
|
||||
doSearch,
|
||||
resetSearch,
|
||||
queryParams,
|
||||
formSchemas,
|
||||
clearSearch,
|
||||
getGroupDatePlaceholder,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.group-query-string {
|
||||
width: 20px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
// 查询条件的边距要和列表对齐,所以查询条件的边距要设为0
|
||||
.jeecg-basic-table-form-container.p-0 {
|
||||
padding: 0;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240514---for:【QQYUN-9241】form表单上下间距大点
|
||||
.jeecg-basic-table-form-container {
|
||||
:deep(.ant-form-item) {
|
||||
&:not(.ant-form-item-with-help) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240514---for:【QQYUN-9241】form表单上下间距大点
|
||||
.online-query-form {
|
||||
:deep(.ant-form) {
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,415 @@
|
||||
<!-- 此文件已没有使用的地方 -->
|
||||
<template>
|
||||
<a-form-item :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
|
||||
<!-- 1.日期 -->
|
||||
<template v-if="item.view == 'date'">
|
||||
<template v-if="single_mode === item.mode">
|
||||
<a-date-picker
|
||||
style="width: 100%"
|
||||
:showTime="false"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
:placeholder="'请选择' + item.label"
|
||||
v-model:value="innerValue"
|
||||
></a-date-picker>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-date-picker
|
||||
:showTime="false"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="开始日期"
|
||||
v-model:value="beginValue"
|
||||
style="width: calc(50% - 15px)"
|
||||
></a-date-picker>
|
||||
<span class="group-query-strig">~</span>
|
||||
<a-date-picker
|
||||
:showTime="false"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="结束日期"
|
||||
v-model:value="endValue"
|
||||
style="width: calc(50% - 15px)"
|
||||
></a-date-picker>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 2.时间 -->
|
||||
<template v-else-if="item.view == 'datetime'">
|
||||
<template v-if="single_mode === item.mode">
|
||||
<a-date-picker
|
||||
style="width: 100%"
|
||||
:showTime="true"
|
||||
valueFormat="YYYY-MM-DD hh:mm:ss"
|
||||
:placeholder="'请选择' + item.label"
|
||||
v-model:value="innerValue"
|
||||
></a-date-picker>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-date-picker
|
||||
:showTime="true"
|
||||
valueFormat="YYYY-MM-DD hh:mm:ss"
|
||||
placeholder="开始时间"
|
||||
v-model:value="beginValue"
|
||||
style="width: calc(50% - 15px)"
|
||||
></a-date-picker>
|
||||
<span class="group-query-strig">~</span>
|
||||
<a-date-picker
|
||||
:showTime="true"
|
||||
valueFormat="YYYY-MM-DD hh:mm:ss"
|
||||
placeholder="结束时间"
|
||||
v-model:value="endValue"
|
||||
style="width: calc(50% - 15px)"
|
||||
></a-date-picker>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 3 TODO 时分秒 -->
|
||||
|
||||
<!-- 4.简单下拉框 -->
|
||||
<template v-else-if="isEasySelect()">
|
||||
<JDictSelectTag v-if="item.config === '1'" :placeholder="'请选择' + item.label" v-model:value="innerValue" :dictCode="getDictCode()" />
|
||||
<a-select v-else :placeholder="'请选择' + item.label" v-model:value="innerValue">
|
||||
<template v-for="(obj, index) in dictOptions[getDictOptionKey(item)]" :key="index">
|
||||
<a-select-option :value="obj.value"> {{ obj.text }}</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<!-- 5.下拉树 -->
|
||||
<template v-else-if="item.view === 'sel_tree'">
|
||||
<JTreeSelect
|
||||
:placeholder="'请选择' + item.label"
|
||||
v-model:value="innerValue"
|
||||
:dict="item.dict"
|
||||
:pidField="item.pidField"
|
||||
:pidValue="item.pidValue"
|
||||
:hasChildField="item.hasChildField"
|
||||
load-triggle-change
|
||||
>
|
||||
</JTreeSelect>
|
||||
</template>
|
||||
|
||||
<!-- 6.分类树 -->
|
||||
<template v-else-if="item.view === 'cat_tree'">
|
||||
<JCategorySelect
|
||||
@change="handleCategoryTreeChange"
|
||||
:loadTriggleChange="true"
|
||||
:pcode="item.pcode"
|
||||
v-model:value="innerValue"
|
||||
:placeholder="'请选择' + item.label"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 7.下拉搜索 -->
|
||||
<template v-else-if="item.view === 'sel_search'">
|
||||
<JDictSelectTag v-if="item.config === '1'" v-model:value="innerValue" :placeholder="'请选择' + item.label" :dict="getDictCode()" />
|
||||
<JOnlineSearchSelect v-else v-model:value="innerValue" :placeholder="'请选择' + item.label" :sql="getSqlByDictCode()" />
|
||||
</template>
|
||||
|
||||
<!-- 8.用户 -->
|
||||
<JSelectUser
|
||||
v-else-if="item.view == 'sel_user'"
|
||||
v-bind="userSelectProp"
|
||||
v-model:value="innerValue"
|
||||
:placeholder="'请选择' + item.label"
|
||||
></JSelectUser>
|
||||
|
||||
<!-- 9.部门 -->
|
||||
<JSelectDept
|
||||
v-else-if="item.view == 'sel_depart'"
|
||||
:showButton="false"
|
||||
v-bind="depSelectProp"
|
||||
v-model:value="innerValue"
|
||||
:placeholder="'请选择' + item.label"
|
||||
/>
|
||||
|
||||
<!-- 10.popup -->
|
||||
<JPopup
|
||||
v-else-if="item.view == 'popup'"
|
||||
:placeholder="'请选择' + item.label"
|
||||
v-model:value="innerValue"
|
||||
:code="item.dictTable"
|
||||
:setFieldsValue="setFieldsValue"
|
||||
:field-config="getPopupFieldConfig(item)"
|
||||
:multi="true"
|
||||
>
|
||||
</JPopup>
|
||||
|
||||
<!-- 11.省市区 -->
|
||||
<JAreaSelect v-else-if="item.view == 'pca'" :placeholder="'请选择' + item.label" v-model:value="innerValue" />
|
||||
|
||||
<!-- 12.下拉多选 -->
|
||||
<template v-else-if="item.view == 'checkbox' || item.view == 'list_multi'" :label="item.label">
|
||||
<JSelectMultiple :dictCode="getDictCode()" :placeholder="'请选择' + item.label" v-model:value="innerValue"></JSelectMultiple>
|
||||
|
||||
<!--<JDictSelectTag mode="multiple" @change="handleSelectChange" :dictCode="getDictCode()"/>-->
|
||||
</template>
|
||||
|
||||
<!-- 13.普通输入框 -->
|
||||
<template v-else>
|
||||
<template v-if="single_mode === item.mode">
|
||||
<a-input :placeholder="'请选择' + item.label" v-model:value="innerValue"></a-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-input placeholder="开始值" v-model:value="beginValue" style="width: calc(50% - 15px)"></a-input>
|
||||
<span class="group-query-strig">~</span>
|
||||
<a-input placeholder="结束值" v-model:value="endValue" style="width: calc(50% - 15px)"></a-input>
|
||||
</template>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, nextTick, ref, unref, watch, toRaw } from 'vue';
|
||||
import {
|
||||
JDictSelectTag,
|
||||
JTreeSelect,
|
||||
JSearchSelect,
|
||||
JCategorySelect,
|
||||
JSelectUserByDept,
|
||||
JSelectDept,
|
||||
JPopup,
|
||||
JAreaLinkage,
|
||||
JSelectUser,
|
||||
JSelectMultiple,
|
||||
JAreaSelect,
|
||||
FormActionType,
|
||||
} from '/@/components/Form';
|
||||
import JOnlineSearchSelect from '../../auto/comp/JOnlineSearchSelect.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OnlineSearchFormItem',
|
||||
components: {
|
||||
JOnlineSearchSelect,
|
||||
JDictSelectTag,
|
||||
JTreeSelect,
|
||||
JCategorySelect,
|
||||
JSelectUser,
|
||||
JSelectUserByDept,
|
||||
JSelectDept,
|
||||
JPopup,
|
||||
JAreaLinkage,
|
||||
JAreaSelect,
|
||||
JSelectMultiple,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true,
|
||||
},
|
||||
dictOptions: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: false,
|
||||
},
|
||||
onlineForm: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:value', 'change'],
|
||||
setup(props, { emit }) {
|
||||
// 定义查询条件 文本label的最大宽度 比起单纯的控制字体个数更好
|
||||
const labelTextMaxWidth = '120px';
|
||||
const labelCol = {
|
||||
style: {
|
||||
'max-width': labelTextMaxWidth,
|
||||
},
|
||||
};
|
||||
const single_mode = 'single';
|
||||
let innerValue = ref<string | undefined | []>('');
|
||||
let beginValue = ref('');
|
||||
let endValue = ref('');
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
if (isEasySelect()) {
|
||||
// 下拉框这里设置空数组 不知道为什么会有警告
|
||||
innerValue.value = !!props.value ? props.value : undefined;
|
||||
} else {
|
||||
innerValue.value = props.value;
|
||||
}
|
||||
if (!props.value) {
|
||||
beginValue.value = '';
|
||||
endValue.value = '';
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
innerValue,
|
||||
(newVal) => {
|
||||
console.log('innerValue-change', newVal);
|
||||
emit('update:value', newVal);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(beginValue, (newVal) => {
|
||||
emit('change', props.item.field + '_begin', newVal);
|
||||
emit('update:value', '1');
|
||||
});
|
||||
|
||||
watch(endValue, (newVal) => {
|
||||
emit('change', props.item.field + '_end', newVal);
|
||||
emit('update:value', '1');
|
||||
});
|
||||
|
||||
function getDictOptionKey(item) {
|
||||
console.log('ddictOptions', props.dictOptions);
|
||||
if (item.dbField) {
|
||||
return item.dbField;
|
||||
} else {
|
||||
return item.field;
|
||||
}
|
||||
}
|
||||
|
||||
function isEasySelect() {
|
||||
let item = props.item;
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
return item.view == 'list' || item.view == 'radio' || item.view == 'switch';
|
||||
}
|
||||
|
||||
function getDictCode() {
|
||||
let item = props.item;
|
||||
if (item.dictTable && item.dictTable.length > 0) {
|
||||
return item.dictTable + ',' + item.dictText + ',' + item.dictCode;
|
||||
} else {
|
||||
return item.dictCode;
|
||||
}
|
||||
}
|
||||
|
||||
function getSqlByDictCode() {
|
||||
let item = props.item;
|
||||
let { dictTable, dictCode, dictText } = item;
|
||||
let temp = dictTable.toLowerCase();
|
||||
let arr = temp.split('where');
|
||||
let condition = '';
|
||||
if (arr.length > 1) {
|
||||
condition = ' where' + arr[1];
|
||||
}
|
||||
let sql = 'select ' + dictCode + " as 'value', " + dictText + " as 'text' from " + arr[0] + condition;
|
||||
console.log('sql', sql);
|
||||
return sql;
|
||||
}
|
||||
|
||||
function getPopupFieldConfig(item) {
|
||||
let { dictText: destFields, dictCode: orgFields } = item;
|
||||
if (!destFields || destFields.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let arr1 = destFields.split(',');
|
||||
let arr2 = orgFields.split(',');
|
||||
let config: any[] = [];
|
||||
for (let i = 0; i < arr1.length; i++) {
|
||||
config.push({
|
||||
target: arr1[i],
|
||||
source: arr2[i],
|
||||
});
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function setFieldsValue<T>(values: T) {
|
||||
let { dictText: destFields } = props.item;
|
||||
let arr1 = destFields.split(',');
|
||||
let field = arr1[0];
|
||||
emit('change', field, values[field]);
|
||||
}
|
||||
|
||||
function handleCategoryTreeChange(value) {
|
||||
emit('update:value', value);
|
||||
}
|
||||
|
||||
function getComponentProps(item, labelKey, rowKey) {
|
||||
let props = {
|
||||
labelKey,
|
||||
rowKey,
|
||||
};
|
||||
let fieldExtendJson = item.fieldExtendJson;
|
||||
if (fieldExtendJson) {
|
||||
if (typeof fieldExtendJson == 'string') {
|
||||
let json = JSON.parse(fieldExtendJson);
|
||||
let extend = { ...json };
|
||||
if (extend.text) {
|
||||
props['labelKey'] = extend.text;
|
||||
}
|
||||
if (extend.store) {
|
||||
props['rowKey'] = extend.store;
|
||||
}
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
let userSelectProp = getComponentProps(props.item, 'realname', 'username');
|
||||
console.log('userSelectProp', userSelectProp);
|
||||
let depSelectProp = getComponentProps(props.item, 'departName', 'id');
|
||||
|
||||
function handleSelectChange(array) {
|
||||
if (array && array.length > 0) {
|
||||
emit('update:value', array.join(','));
|
||||
} else {
|
||||
emit('update:value', '');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getPopupFieldConfig,
|
||||
userSelectProp,
|
||||
depSelectProp,
|
||||
handleSelectChange,
|
||||
setFieldsValue,
|
||||
innerValue,
|
||||
beginValue,
|
||||
endValue,
|
||||
isEasySelect,
|
||||
getDictOptionKey,
|
||||
getDictCode,
|
||||
labelTextMaxWidth,
|
||||
labelCol,
|
||||
single_mode,
|
||||
getSqlByDictCode,
|
||||
handleCategoryTreeChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.group-query-strig {
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
/* 查询条件左对齐样式设置 */
|
||||
.jeecg-online-search :deep(.ant-form-item-label) {
|
||||
flex: 0 0 auto !important;
|
||||
width: auto;
|
||||
}
|
||||
.jeecg-online-search :deep(.ant-form-item-control) {
|
||||
max-width: 100%;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
/* label显示宽度 超出显示... */
|
||||
.jeecg-online-search :deep(.label-text) {
|
||||
max-width: v-bind(labelTextMaxWidth);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<!-- 级联下拉框 form组件 暂且只在online使用 不对外提供api -->
|
||||
<a-select show-search :filter-option="filterOption" :placeholder="placeholder" :value="selectedValue" @change="handleChange" allowClear style="width: 100%">
|
||||
<a-select-option v-for="(item, index) in dictOptions" :key="index" :value="item.store">
|
||||
<span style="display: inline-block; width: 100%" :title="item.label">{{ item.label }}</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch, ref } from 'vue';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
/**获取下拉选项*/
|
||||
const SELECT_OPTIONS_URL = '/online/cgform/api/querySelectOptions';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OnlineSelectCascade',
|
||||
props: {
|
||||
table: { type: String, default: '' },
|
||||
txt: { type: String, default: '' },
|
||||
store: { type: String, default: '' },
|
||||
idField: { type: String, default: '' },
|
||||
pidField: { type: String, default: '' },
|
||||
pidValue: { type: String, default: '-1' },
|
||||
origin: { type: Boolean, default: false },
|
||||
condition: { type: String, default: '' },
|
||||
value: { type: String, default: '' },
|
||||
isNumber: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '请选择' },
|
||||
},
|
||||
emits: ['change', 'next'],
|
||||
setup(props, { emit }) {
|
||||
const { createMessage: $message } = useMessage();
|
||||
// 选中值
|
||||
const selectedValue = ref<any>('');
|
||||
// 选项数组
|
||||
const dictOptions = ref<any[]>([]);
|
||||
const optionsLoad = ref(true);
|
||||
// 选项改变事件
|
||||
function handleChange(value) {
|
||||
console.log('handleChange', value);
|
||||
// 这个value是 存储的值 实际还需要获取id值
|
||||
let temp = value || '';
|
||||
emit('change', temp);
|
||||
valueChangeThenEmitNext(temp);
|
||||
}
|
||||
|
||||
// 第一个节点 选项加载走condition
|
||||
watch(
|
||||
() => props.condition,
|
||||
(val) => {
|
||||
optionsLoad.value = true;
|
||||
if (val) {
|
||||
loadOptions();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 被联动节点 选项加载走pidValue
|
||||
watch(
|
||||
() => props.pidValue,
|
||||
(val) => {
|
||||
if (val === '-1') {
|
||||
dictOptions.value = [];
|
||||
} else {
|
||||
loadOptions();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 值回显
|
||||
watch(
|
||||
() => props.value,
|
||||
(newVal, oldVal) => {
|
||||
console.log('值改变事件', newVal, oldVal);
|
||||
if (!newVal) {
|
||||
// value不存在的时候--
|
||||
selectedValue.value = [];
|
||||
if (oldVal) {
|
||||
// 如果oldVal存在, 需要往上抛事件
|
||||
emit('change', '');
|
||||
emit('next', '-1');
|
||||
}
|
||||
} else {
|
||||
// value存在的时候
|
||||
selectedValue.value = newVal;
|
||||
}
|
||||
if (newVal && !oldVal) {
|
||||
// 有新值没有旧值 表单第一次加载赋值 需要往外抛一个事件 触发下级options的加载
|
||||
handleFirstValueSetting(newVal);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 第一次加载赋值
|
||||
*/
|
||||
async function handleFirstValueSetting(value) {
|
||||
if (props.idField === props.store) {
|
||||
// 如果id字段就是存储字段 那么可以不用调用请求
|
||||
emit('next', value);
|
||||
} else {
|
||||
if (props.origin === true) {
|
||||
// 如果是联动组件的第一个组件,等待options加载完后从options中取值
|
||||
await getSelfOptions();
|
||||
valueChangeThenEmitNext(value);
|
||||
} else {
|
||||
// 如果是联动组件的后续组件,根据选中的value加载一遍数据
|
||||
let arr = await loadValueText();
|
||||
valueChangeThenEmitNext(value, arr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadOptions() {
|
||||
let params = getQueryParams();
|
||||
if (props.origin === true) {
|
||||
params['condition'] = props.condition;
|
||||
} else {
|
||||
params['pidValue'] = props.pidValue;
|
||||
}
|
||||
console.log('请求参数', params);
|
||||
dictOptions.value = [];
|
||||
defHttp.get({ url: SELECT_OPTIONS_URL, params }, { isTransformResponse: false }).then((res) => {
|
||||
if (res.success) {
|
||||
dictOptions.value = [...res.result];
|
||||
console.log('请求结果', res.result, dictOptions);
|
||||
} else {
|
||||
$message.warning('联动组件数据加载失败,请检查配置!');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getQueryParams() {
|
||||
let params = {
|
||||
table: props.table,
|
||||
txt: props.txt,
|
||||
key: props.store,
|
||||
idField: props.idField,
|
||||
pidField: props.pidField,
|
||||
};
|
||||
return params;
|
||||
}
|
||||
|
||||
function loadValueText() {
|
||||
return new Promise((resolve) => {
|
||||
if (!props.value) {
|
||||
selectedValue.value = [];
|
||||
resolve([]);
|
||||
} else {
|
||||
let params = getQueryParams();
|
||||
if (props.isNumber === true) {
|
||||
params['condition'] = `${props.store} = ${props.value}`;
|
||||
} else {
|
||||
params['condition'] = `${props.store} = '${props.value}'`;
|
||||
}
|
||||
defHttp.get({ url: SELECT_OPTIONS_URL, params }, { isTransformResponse: false }).then((res) => {
|
||||
if (res.success) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
$message.warning('联动组件数据加载失败,请检查配置!');
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下拉选项
|
||||
*/
|
||||
function getSelfOptions() {
|
||||
return new Promise((resolve) => {
|
||||
let index = 0;
|
||||
(function next(index) {
|
||||
if (index > 10) {
|
||||
resolve([]);
|
||||
}
|
||||
let arr = dictOptions.value;
|
||||
if (arr && arr.length > 0) {
|
||||
resolve(arr);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
next(index++);
|
||||
}, 300);
|
||||
}
|
||||
})(index);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 值改变后 需要往外抛事件 触发下级节点的选项改变
|
||||
*/
|
||||
function valueChangeThenEmitNext(value, arr: any = []) {
|
||||
if (value && value.length > 0) {
|
||||
if (!arr || arr.length == 0) {
|
||||
arr = dictOptions.value;
|
||||
}
|
||||
let selected = arr.filter((item) => item.store === value);
|
||||
if (selected && selected.length > 0) {
|
||||
let id = selected[0].id;
|
||||
emit('next', id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下拉框筛选
|
||||
* @param input
|
||||
* @param option
|
||||
*/
|
||||
const filterOption = (input: string, option: any) => {
|
||||
let labelIf = option.children()[0]?.children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
if (labelIf) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
selectedValue,
|
||||
dictOptions,
|
||||
handleChange,
|
||||
filterOption,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<BasicForm ref="onlineFormRef" @register="registerForm" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { computed, defineComponent, ref, unref, watch, nextTick, toRaw } from 'vue';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { SUBMIT_FLOW_ID, SUBMIT_FLOW_KEY, VALIDATE_FAILED } from '../../types/onlineRender';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { pick } from 'lodash-es';
|
||||
import { useFormItems, getRefPromise } from '../../hooks/auto/useAutoForm';
|
||||
import { Loading } from '/@/components/Loading';
|
||||
import { loadFormFieldsDefVal } from '../../util/FieldDefVal';
|
||||
|
||||
const urlObject = {
|
||||
optPre: '/online/cgform/api/form/',
|
||||
urlButtonAction: '/online/cgform/api/doButton',
|
||||
};
|
||||
const baseUrl = '/online/cgform/api/subform';
|
||||
export default {
|
||||
name: 'OnlineSubForm',
|
||||
components: {
|
||||
BasicForm,
|
||||
Loading,
|
||||
},
|
||||
props: {
|
||||
properties: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mainId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
table: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
formTemplate: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
requiredFields: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
isUpdate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['formChange'],
|
||||
setup(props, { emit }) {
|
||||
console.log('进入online子表表单页面》》》》' + props.table);
|
||||
|
||||
// 表单ref
|
||||
const onlineFormRef = ref(null);
|
||||
// 表单是否渲染完成
|
||||
const formRendered = ref(false);
|
||||
const { createMessage: $message } = useMessage();
|
||||
const {
|
||||
formSchemas,
|
||||
defaultValueFields,
|
||||
changeDataIfArray2String,
|
||||
tableName,
|
||||
dbData,
|
||||
checkOnlyFieldValue,
|
||||
fieldDisplayStatus,
|
||||
createFormSchemas,
|
||||
baseColProps,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
labelWidth,
|
||||
} = useFormItems(props, onlineFormRef);
|
||||
//表单配置
|
||||
const [registerForm, { setProps, validate, resetFields, setFieldsValue, getFieldsValue, updateSchema, scrollToField }] = useForm({
|
||||
schemas: formSchemas,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: baseColProps,
|
||||
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-7632】 label栅格改成labelwidth固宽
|
||||
labelWidth,
|
||||
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-7632】 label栅格改成labelwidth固宽
|
||||
// update-begin--author:liaozhiyang---date:20240105---for:【QQYUN-7499】多列风格富文本、markdown增加独占一行功能
|
||||
labelCol,
|
||||
wrapperCol
|
||||
// update-end--author:liaozhiyang---date:20240105---for:【QQYUN-7499】多列风格富文本、markdown增加独占一行功能
|
||||
});
|
||||
const getFormItem = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
defHttp.get({ url: `online/cgform/api/getFormItem/${props.id}` }, { isTransformResponse: false }).then((res) => {
|
||||
resolve(res.result);
|
||||
});
|
||||
});
|
||||
}
|
||||
let extConfigJson;
|
||||
watch(
|
||||
() => props.table,
|
||||
() => {
|
||||
tableName.value = props.table;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
//监听配置改变事件
|
||||
watch(
|
||||
() => props.properties,
|
||||
async (valueObj) => {
|
||||
//重新渲染表单
|
||||
console.log('主表properties改变', props.properties);
|
||||
formRendered.value = false;
|
||||
addFormChangeEvent();
|
||||
// update-begin--author:liaozhiyang---date:20260318---for:【issues/9414】一对一子表设置label长度不生效
|
||||
if (!extConfigJson) {
|
||||
try {
|
||||
const data = await getFormItem();
|
||||
extConfigJson = JSON.parse(data.head.extConfigJson);
|
||||
extConfigJson = {
|
||||
formLabelLength: extConfigJson.formLabelLength,
|
||||
formLabelLengthShow: extConfigJson.formLabelLengthShow,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20260318---for:【issues/9414】一对一子表设置label长度不生效
|
||||
createFormSchemas(props.properties, props.requiredFields, checkOnlyFieldValue, extConfigJson);
|
||||
formRendered.value = true;
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
//监听主表数据ID
|
||||
watch(
|
||||
() => props.mainId,
|
||||
(valueObj) => {
|
||||
//重新加载子表数据
|
||||
console.log('主表ID改变', props.mainId);
|
||||
// 此处延迟100毫秒是为了让properties的监听先执行
|
||||
setTimeout(() => {
|
||||
resetSubForm();
|
||||
}, 100);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(val) => {
|
||||
setProps({ disabled: val });
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 监听表单改变事件
|
||||
*/
|
||||
async function addFormChangeEvent() {
|
||||
let formRefObject = await getRefPromise(onlineFormRef);
|
||||
formRefObject.$formValueChange = (field, value, changeFormData) => {
|
||||
let emitArgument = { [field]: value };
|
||||
// update-begin--author:liaozhiyang---date:20260317---for:【QQYUN-9441】online一对多加上关联记录和他表字段
|
||||
// 一对一子表 关联记录和他表字段
|
||||
if(changeFormData){
|
||||
setFieldsValue(changeFormData);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20260317---for:【QQYUN-9441】online一对多加上关联记录和他表字段
|
||||
emit('formChange', emitArgument);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前表单默认值逻辑-进入新增页面触发
|
||||
*/
|
||||
function handleDefaultValue() {
|
||||
if (unref(props.isUpdate) === false) {
|
||||
let fieldProperties = toRaw(defaultValueFields[tableName.value]);
|
||||
loadFormFieldsDefVal(fieldProperties, (values) => {
|
||||
setFieldsValue(values);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当主表数据ID发生改变,子表重现获取数据
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function resetSubForm() {
|
||||
//TODO 填值规则
|
||||
// update-begin--author:sunjianlei --- date:20191111 --- for: 每次加载数据的时候都重新执行一遍填值规则 -----------
|
||||
//this.$emit('executeFillRule', {form:this.form, target: this})
|
||||
// update-end--author:sunjianlei --- date:20191111 --- for: 每次加载数据的时候都重新执行一遍填值规则 -----------
|
||||
await getRefPromise(formRendered);
|
||||
await resetFields();
|
||||
handleDefaultValue();
|
||||
const { table, mainId } = props;
|
||||
if (!table || !mainId) {
|
||||
return;
|
||||
}
|
||||
let values = await loadData(table, mainId);
|
||||
dbData.value = values;
|
||||
// VUEN-1033
|
||||
await setFieldsValue(values);
|
||||
}
|
||||
|
||||
function loadData(table, mainId) {
|
||||
let url = `${baseUrl}/${table}/${mainId}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
defHttp.get({ url }, { isTransformResponse: false }).then((res) => {
|
||||
console.log(res);
|
||||
if (res.success) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
console.log(res.message);
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}).finally(() => {
|
||||
//resetFields()
|
||||
dbData.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
function getAll() {
|
||||
return new Promise((resolve, reject) => {
|
||||
validate()
|
||||
.then(() => {
|
||||
let formData = getFieldsValue();
|
||||
formData = changeDataIfArray2String(formData);
|
||||
resolve(formData);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.errorFields) {
|
||||
e.scrollToField = () => e.errorFields[0] && scrollToField(e.errorFields[0].name, { behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//获取表单事件对象 监听表单改变用到
|
||||
function getFormEvent() {
|
||||
let row = getFieldsValue();
|
||||
if (!row.id) {
|
||||
row.id = 'sub-change-temp-id';
|
||||
}
|
||||
return {
|
||||
row,
|
||||
target: context,
|
||||
};
|
||||
}
|
||||
|
||||
//设置表单的值
|
||||
function setValues(values) {
|
||||
setFieldsValue(values);
|
||||
}
|
||||
|
||||
function executeFillRule() {
|
||||
let formData = getFieldsValue();
|
||||
let fieldProperties = toRaw(defaultValueFields[tableName.value]);
|
||||
loadFormFieldsDefVal(fieldProperties, (values) => {
|
||||
setFieldsValue(values);
|
||||
}, formData);
|
||||
}
|
||||
|
||||
const context = {
|
||||
onlineFormRef,
|
||||
baseColProps,
|
||||
formSchemas,
|
||||
registerForm,
|
||||
setFieldsValue,
|
||||
getFieldsValue,
|
||||
getFormEvent,
|
||||
setValues,
|
||||
getAll,
|
||||
executeFillRule,
|
||||
sh: fieldDisplayStatus,
|
||||
resetFields,
|
||||
updateSchema,
|
||||
};
|
||||
return context;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// update-begin--author:liaozhiyang---date:20240527---for:【TV360X-263】tab风格一对一子表上传组件有数据没渲染出来
|
||||
:deep(.ant-upload-list-item-container) {
|
||||
&.ant-motion-collapse {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240527---for:【TV360X-263】tab风格一对一子表上传组件有数据没渲染出来
|
||||
</style>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<detail-form :schemas="detailFormSchemas" :data="subFormData" :span="formSpan"></detail-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { ref, watch } from 'vue';
|
||||
import { BasicForm } from '/@/components/Form/index';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { getRefPromise } from '../../hooks/auto/useAutoForm';
|
||||
import { Loading } from '/@/components/Loading';
|
||||
import DetailForm from '../../extend/form/DetailForm.vue';
|
||||
import { getDetailFormSchemas } from '../../hooks/auto/useAutoForm';
|
||||
|
||||
const baseUrl = '/online/cgform/api/subform';
|
||||
export default {
|
||||
name: 'OnlineSubFormDetail',
|
||||
components: {
|
||||
BasicForm,
|
||||
Loading,
|
||||
DetailForm,
|
||||
},
|
||||
props: {
|
||||
properties: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mainId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
table: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
formTemplate: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
emits: ['formChange'],
|
||||
setup(props) {
|
||||
// 表单是否渲染完成
|
||||
const formRendered = ref(false);
|
||||
const { createMessage: $message } = useMessage();
|
||||
const tableName = ref('');
|
||||
const subFormData = ref<any>({});
|
||||
const { detailFormSchemas, createFormSchemas, formSpan } = getDetailFormSchemas(props);
|
||||
|
||||
watch(
|
||||
() => props.table,
|
||||
() => {
|
||||
tableName.value = props.table;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
//监听配置改变事件
|
||||
watch(
|
||||
() => props.properties,
|
||||
() => {
|
||||
//重新渲染表单
|
||||
console.log('主表properties改变', props.properties);
|
||||
formRendered.value = false;
|
||||
createFormSchemas(props.properties);
|
||||
formRendered.value = true;
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
//监听主表数据ID
|
||||
watch(
|
||||
() => props.mainId,
|
||||
() => {
|
||||
//重新加载子表数据
|
||||
console.log('主表ID改变', props.mainId);
|
||||
// 此处延迟100毫秒是为了让properties的监听先执行
|
||||
setTimeout(() => {
|
||||
resetSubForm();
|
||||
}, 100);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 当主表数据ID发生改变,子表重现获取数据
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function resetSubForm() {
|
||||
await getRefPromise(formRendered);
|
||||
subFormData.value = {};
|
||||
const { table, mainId } = props;
|
||||
if (!table || !mainId) {
|
||||
return;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20260413---for:【QQYUN-14951】一对一他表字段详情没值
|
||||
let data: any = (await loadData(table, mainId)) || {};
|
||||
await fillLinkTableFields(data);
|
||||
subFormData.value = data;
|
||||
// update-end--author:liaozhiyang---date:20260413---for:【QQYUN-14951】一对一他表字段详情没值
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20260413---for:【QQYUN-14951】一对一他表字段详情没值
|
||||
/**
|
||||
* 详情页一对一子表中,根据关联记录字段(link_table)的值查询关联数据,自动填充他表字段(link_table_field)的值
|
||||
*/
|
||||
async function fillLinkTableFields(data) {
|
||||
const schemas = detailFormSchemas.value;
|
||||
for (const schema of schemas) {
|
||||
if (schema.view === 'link_table' && (schema as any).linkFields?.length > 0) {
|
||||
const fieldValue = data[schema.field];
|
||||
if (fieldValue) {
|
||||
const valueField = (schema as any).dictCode || 'id';
|
||||
const vals = String(fieldValue).split(',');
|
||||
const params = {
|
||||
pageSize: vals.length,
|
||||
pageNo: 1,
|
||||
superQueryMatchType: 'and',
|
||||
superQueryParams: encodeURI(JSON.stringify([{ field: valueField, rule: 'in', val: fieldValue }])),
|
||||
};
|
||||
try {
|
||||
const result = await defHttp.get({ url: '/online/cgform/api/getData/' + (schema as any).dictTable, params });
|
||||
const records = result?.records || [];
|
||||
for (const linkField of (schema as any).linkFields) {
|
||||
const [formField, tableField] = linkField.split(',');
|
||||
if (records.length > 0) {
|
||||
data[formField] = records.map((r: any) => r[tableField] ?? '').join(',');
|
||||
} else {
|
||||
data[formField] = '';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('填充他表字段失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20260413---for:【QQYUN-14951】一对一他表字段详情没值
|
||||
|
||||
|
||||
async function loadData(table, mainId) {
|
||||
let url = `${baseUrl}/${table}/${mainId}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
defHttp.get({ url }, { isTransformResponse: false }).then((res) => {
|
||||
console.log(res);
|
||||
if (res.success) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
reject(res.message);
|
||||
}
|
||||
});
|
||||
}).catch((e) => {
|
||||
console.warn('子表获取数据失败:', e);
|
||||
return Promise.resolve({});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
detailFormSchemas,
|
||||
subFormData,
|
||||
formSpan,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,177 @@
|
||||
import InputWidget from './impl/InputWidget';
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import DateWidget from './impl/DateWidget';
|
||||
import SelectWidget from './impl/SelectWidget';
|
||||
import PasswordWidget from './impl/PasswordWidget';
|
||||
import FileWidget from './impl/FileWidget';
|
||||
import ImageWidget from './impl/ImageWidget';
|
||||
import TextAreaWidget from './impl/TextAreaWidget';
|
||||
import SelectMultiWidget from './impl/SelectMultiWidget';
|
||||
import SelectSearchWidget from './impl/SelectSearchWidget';
|
||||
import PopupWidget from './impl/PopupWidget';
|
||||
// update-begin--author:liaozhiyang---date:20240130---for:【QQYUN-7961】popupDict字典
|
||||
import PopupDictWidget from './impl/PopupDictWidget';
|
||||
// update-end--author:liaozhiyang---date:20240130---for:【QQYUN-7961】popupDict字典
|
||||
import TreeCategoryWidget from './impl/TreeCategoryWidget';
|
||||
import SelectDepartWidget from './impl/SelectDepartWidget';
|
||||
import SelectUserWidget from './impl/SelectUserWidget';
|
||||
import EditorWidget from './impl/EditorWidget';
|
||||
import MarkdownWidget from './impl/MarkdownWidget';
|
||||
import PcaWidget from './impl/PcaWidget';
|
||||
import AreaLinkage from './impl/AreaLinkage';
|
||||
import TreeSelectWidget from './impl/TreeSelectWidget';
|
||||
import RadioWidget from './impl/RadioWidget';
|
||||
import CheckboxWidget from './impl/CheckboxWidget';
|
||||
import SwitchWidget from './impl/SwitchWidget';
|
||||
import TimeWidget from './impl/TimeWidget';
|
||||
import LinkDownWidget from './impl/LinkDownWidget';
|
||||
import SlotWidget from './impl/SlotWidget';
|
||||
import NumberWidget from './impl/NumberWidget';
|
||||
import LinkTableWidget from './impl/LinkTableWidget'
|
||||
import LinkTableFieldWidget from './impl/LinkTableFieldWidget'
|
||||
import LinkTableForQueryWidget from './impl/LinkTableForQueryWidget'
|
||||
import CascaderPcaForQueryWidget from './impl/CascaderPcaForQueryWidget'
|
||||
import SelectUser2Widget from './impl/SelectUser2Widget'
|
||||
import RangeWidget from "./impl/RangeWidget";
|
||||
|
||||
export default class FormSchemaFactory {
|
||||
static createFormSchema(key, data, queryItem) {
|
||||
let view = data.view;
|
||||
switch (view) {
|
||||
case 'password':
|
||||
//2.密码输入框
|
||||
return new PasswordWidget(key, data);
|
||||
case 'list':
|
||||
//3.下拉框
|
||||
return new SelectWidget(key, data);
|
||||
case 'radio':
|
||||
// 4. 单选
|
||||
return new RadioWidget(key, data);
|
||||
case 'checkbox':
|
||||
// 5.多选
|
||||
return new CheckboxWidget(key, data);
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
// 6.日期
|
||||
// 7.日期时间
|
||||
return new DateWidget(key, data, queryItem);
|
||||
case 'time':
|
||||
// 8 时间
|
||||
return new TimeWidget(key, data);
|
||||
case 'file':
|
||||
// 9.文件
|
||||
return new FileWidget(key, data);
|
||||
case 'image':
|
||||
// 10.图片
|
||||
return new ImageWidget(key, data);
|
||||
case 'textarea':
|
||||
// 11.多行文本
|
||||
return new TextAreaWidget(key, data);
|
||||
case 'list_multi':
|
||||
// 12.下拉多选框
|
||||
return new SelectMultiWidget(key, data);
|
||||
case 'sel_search':
|
||||
// 13.下拉搜索框
|
||||
return new SelectSearchWidget(key, data);
|
||||
case 'popup':
|
||||
// 14. popup
|
||||
return new PopupWidget(key, data);
|
||||
case 'cat_tree':
|
||||
// 15.分类字典树
|
||||
return new TreeCategoryWidget(key, data);
|
||||
case 'sel_depart':
|
||||
// 16.部门选择
|
||||
return new SelectDepartWidget(key, data);
|
||||
case 'sel_user':
|
||||
// 17.用户选择
|
||||
return new SelectUserWidget(key, data);
|
||||
case 'umeditor':
|
||||
// 18.富文本
|
||||
return new EditorWidget(key, data);
|
||||
case 'markdown':
|
||||
// 19.MarkDown
|
||||
return new MarkdownWidget(key, data);
|
||||
case 'pca':
|
||||
// 20.省市区
|
||||
// update-begin--author:liaozhiyang---date:20240607---for:【TV360X-501】省市区换新组件
|
||||
// return new PcaWidget(key, data);
|
||||
return new AreaLinkage(key, data);
|
||||
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-501】省市区换新组件
|
||||
case 'link_down':
|
||||
// 21.联动组件
|
||||
return new LinkDownWidget(key, data);
|
||||
case 'sel_tree':
|
||||
// 22.自定义树控件
|
||||
return new TreeSelectWidget(key, data);
|
||||
case 'switch':
|
||||
// 23.开关组件
|
||||
return new SwitchWidget(key, data);
|
||||
case 'link_table':
|
||||
// 24.关联记录
|
||||
return new LinkTableWidget(key, data);
|
||||
case 'link_table_field':
|
||||
// 25.他表字段
|
||||
return new LinkTableFieldWidget(key, data);
|
||||
// update-begin--author:liaozhiyang---date:20240130---for:【QQYUN-7961】popupDict字典
|
||||
case 'popup_dict':
|
||||
// 14. popup字典
|
||||
return new PopupDictWidget(key, data);
|
||||
// update-end--author:liaozhiyang---date:20240130---for:【QQYUN-7961】popupDict字典
|
||||
case 'slot':
|
||||
// slot
|
||||
return new SlotWidget(key, data);
|
||||
case 'LinkTableForQuery':
|
||||
return new LinkTableForQueryWidget(key, data);
|
||||
case 'CascaderPcaForQuery':
|
||||
return new CascaderPcaForQueryWidget(key, data, queryItem);
|
||||
case 'select_user2':
|
||||
return new SelectUser2Widget(key, data);
|
||||
case 'rangeDate':
|
||||
case 'rangeTime':
|
||||
case 'rangeNumber':
|
||||
return new RangeWidget(key, data);
|
||||
case 'hidden':
|
||||
// 隐藏的控件 如分类树的文本
|
||||
return new InputWidget(key, data).isHidden();
|
||||
default:
|
||||
if (data.type == 'number') {
|
||||
return new NumberWidget(key, data);
|
||||
} else {
|
||||
//1.普通输入框
|
||||
return new InputWidget(key, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static createSlotFormSchema(key, data) {
|
||||
let slotFs = new SlotWidget(key, data);
|
||||
let view = data.view;
|
||||
if ('date' == view) {
|
||||
slotFs.groupDate();
|
||||
} else if ('datetime' == view) {
|
||||
slotFs.groupDatetime();
|
||||
} else if ('time' == view) {
|
||||
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9348】增加online查询区域时间范围查询功能
|
||||
slotFs.groupTime();
|
||||
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9348】增加online查询区域时间范围查询功能
|
||||
} else {
|
||||
let type = data.type;
|
||||
if (type == 'number' || type == 'integer') {
|
||||
slotFs.groupNumber();
|
||||
}
|
||||
}
|
||||
return slotFs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单ID 默认是隐藏的
|
||||
*/
|
||||
static createIdField(): FormSchema {
|
||||
return {
|
||||
label: '',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
import {computed, watch} from 'vue'
|
||||
import { FormSchema, Rule } from '/@/components/Form';
|
||||
import { FieldExtends, POP_CONTAINER } from '../../../types/onlineRender';
|
||||
import { LABELLENGTH } from '../../../util/constant';
|
||||
import {replaceUserInfoByExpression} from "@/utils/common/compUtils";
|
||||
/**
|
||||
* 1.部门选择/用户选择 无:单选配置
|
||||
* 控件类
|
||||
*/
|
||||
export default abstract class IFormSchema {
|
||||
_data;
|
||||
field: string;
|
||||
label: string;
|
||||
labelLength: number;
|
||||
formRef: any;
|
||||
hidden: boolean;
|
||||
order: number;
|
||||
required: boolean;
|
||||
onlyValidator: any;
|
||||
hasChange: boolean;
|
||||
pre: string;
|
||||
setFieldsValue: any;
|
||||
schemaProp: any;
|
||||
searchForm: boolean;
|
||||
disabled: boolean;
|
||||
popContainer: string;
|
||||
inPopover: boolean;
|
||||
|
||||
constructor(key, data) {
|
||||
// 考虑不需要存data
|
||||
this._data = data;
|
||||
this.field = key;
|
||||
this.label = data.title;
|
||||
this.hidden = false;
|
||||
this.order = data.order || 999;
|
||||
this.required = false;
|
||||
this.onlyValidator = '';
|
||||
this.setFieldsValue = '';
|
||||
this.hasChange = true;
|
||||
if (key.indexOf('@') > 0) {
|
||||
this.pre = key.substring(0, key.indexOf('@') + 1);
|
||||
} else {
|
||||
this.pre = '';
|
||||
}
|
||||
this.schemaProp = {};
|
||||
this.searchForm = false;
|
||||
this.disabled = false;
|
||||
this.popContainer = '';
|
||||
this.handleWidgetAttr(data);
|
||||
this.inPopover = false;
|
||||
this.labelLength = LABELLENGTH;
|
||||
this.initLabelLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最终的表单配置项,外面获取调用此方法
|
||||
*/
|
||||
getFormItemSchema(): FormSchema {
|
||||
let schema = this.getItem();
|
||||
this.addDefaultChangeEvent(schema);
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表单配置,子类重写此方法
|
||||
*/
|
||||
getItem(): FormSchema {
|
||||
let fs: FormSchema = {
|
||||
field: this.field,
|
||||
label: this.label,
|
||||
labelLength: this.labelLength,
|
||||
component: 'Input',
|
||||
itemProps:{
|
||||
labelCol:{
|
||||
class: 'online-form-label'
|
||||
}
|
||||
}
|
||||
};
|
||||
let rules = this.getRule();
|
||||
if (rules.length > 0 && this.onlyValidator) {
|
||||
fs['rules'] = rules;
|
||||
}
|
||||
if (this.hidden === true) {
|
||||
fs['show'] = false;
|
||||
}
|
||||
return fs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置表单ref
|
||||
* popup、分类树需要关联设置其他表单值的时候用到
|
||||
* @param ref
|
||||
*/
|
||||
setFormRef(ref) {
|
||||
this.formRef = ref;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置表单元素隐藏
|
||||
*/
|
||||
isHidden() {
|
||||
this.hidden = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否必填项
|
||||
* @param array
|
||||
*/
|
||||
isRequired(array) {
|
||||
// 子表必填 TODO
|
||||
if (array && array.length > 0) {
|
||||
if (array.indexOf(this.field) >= 0) {
|
||||
this.required = true;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 label长度
|
||||
*/
|
||||
initLabelLength(){
|
||||
let obj = this.getExtendData()
|
||||
if(obj && obj.labelLength){
|
||||
this.labelLength = obj.labelLength;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展参数
|
||||
*/
|
||||
getExtendData() {
|
||||
let extend: FieldExtends = {};
|
||||
let { fieldExtendJson } = this._data;
|
||||
if (fieldExtendJson) {
|
||||
if (typeof fieldExtendJson == 'string') {
|
||||
try {
|
||||
let json = JSON.parse(fieldExtendJson);
|
||||
extend = { ...json };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return extend;
|
||||
}
|
||||
|
||||
/***
|
||||
* 获取和此字段相关的其他字段 需要设置其为隐藏
|
||||
*/
|
||||
getRelatedHideFields(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* placeholder
|
||||
*/
|
||||
getPlaceholder(view) {
|
||||
let text = '请输入';
|
||||
// update-begin--author:liaozhiyang---date:20240521---for:【TV360X-218】针对组件分别提示对应的校验语
|
||||
if (
|
||||
[
|
||||
'list',
|
||||
'radio',
|
||||
'checkbox',
|
||||
'date',
|
||||
'datetime',
|
||||
'time',
|
||||
'list_multi',
|
||||
'sel_search',
|
||||
'popup',
|
||||
'cat_tree',
|
||||
'sel_depart',
|
||||
'sel_user',
|
||||
'pca',
|
||||
'link_down',
|
||||
'sel_tree',
|
||||
'switch',
|
||||
'link_table',
|
||||
'link_table_field',
|
||||
'popup_dict',
|
||||
'LinkTableForQuery',
|
||||
'CascaderPcaForQuery',
|
||||
'select_user2',
|
||||
'rangeDate',
|
||||
'rangeTime',
|
||||
'rangeNumber',
|
||||
].includes(view)
|
||||
) {
|
||||
text = '请选择';
|
||||
} else if (['file', 'image'].includes(view)) {
|
||||
text = '请上传';
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240521---for:【TV360X-218】针对组件分别提示对应的校验语
|
||||
return text + this.label;
|
||||
}
|
||||
|
||||
/**
|
||||
* 唯一校验
|
||||
*/
|
||||
setOnlyValidateFun(validateFun) {
|
||||
if (validateFun) {
|
||||
this.onlyValidator = async (rule, value) => {
|
||||
let error = await validateFun(rule, value);
|
||||
if (!error) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取校验规则
|
||||
*/
|
||||
getRule(): any[] {
|
||||
let rules: Rule[] = [];
|
||||
const { view, errorInfo, pattern, type, fieldExtendJson } = this._data;
|
||||
if (this.required === true) {
|
||||
let msg = this.getPlaceholder(view);
|
||||
// update-begin--author:liaozhiyang---date:20240520---for:【TV360X-80】扩展参数配置中的校验提示不生效
|
||||
if (fieldExtendJson) {
|
||||
const json = JSON.parse(fieldExtendJson);
|
||||
if (json.validateError) {
|
||||
msg = json.validateError;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240520---for:【TV360X-80】扩展参数配置中的校验提示不生效
|
||||
if (errorInfo) {
|
||||
msg = errorInfo;
|
||||
}
|
||||
if (view == 'sel_depart' || view == 'sel_user') {
|
||||
//如果是部门和用户组件 使用 required:true
|
||||
this.schemaProp['required'] = true;
|
||||
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-9109】online使用部门和用户组件必填时label前面没有必填的*号
|
||||
rules.push({ required: true, message: msg });
|
||||
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-9109】online使用部门和用户组件必填时label前面没有必填的*号
|
||||
} else {
|
||||
rules.push({ required: true, message: msg });
|
||||
}
|
||||
}
|
||||
if ('sel_user' == view) {
|
||||
if (pattern === 'only' && this.onlyValidator) {
|
||||
rules.push({ validator: this.onlyValidator });
|
||||
}
|
||||
}
|
||||
if ('list' === view || 'radio' === view || 'markdown' === view || 'pca' === view || view.indexOf('sel') >= 0 || 'time' === view) {
|
||||
return rules;
|
||||
}
|
||||
if (view.indexOf('upload') >= 0 || view.indexOf('file') >= 0 || view.indexOf('image') >= 0) {
|
||||
return rules;
|
||||
}
|
||||
if (pattern) {
|
||||
if (pattern === 'only') {
|
||||
if (this.onlyValidator) {
|
||||
rules.push({ validator: this.onlyValidator });
|
||||
}
|
||||
} else if (pattern === 'z') {
|
||||
if (type == 'number' || type == 'integer') {
|
||||
// this.onlyInteger=true TODO
|
||||
} else {
|
||||
rules.push({ pattern: /^-?\d+$/, message: '请输入整数' });
|
||||
}
|
||||
} else {
|
||||
let msg = errorInfo || '正则校验失败';
|
||||
let reg
|
||||
try {
|
||||
reg = new RegExp(pattern);
|
||||
if (!reg) {
|
||||
reg = pattern;
|
||||
}
|
||||
} catch {
|
||||
reg = pattern;
|
||||
}
|
||||
rules.push({ pattern: reg, message: msg });
|
||||
}
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加默认的change事件
|
||||
* @param schema
|
||||
*/
|
||||
addDefaultChangeEvent(schema) {
|
||||
if (this.hasChange) {
|
||||
if (!schema.componentProps) {
|
||||
schema.componentProps = {};
|
||||
}
|
||||
//update-begin-author:taoyan date:2022-5-24 for: VUEN-1095 只读未控制住
|
||||
if (this.disabled == true) {
|
||||
schema.componentProps.disabled = true;
|
||||
}
|
||||
//update-end-author:taoyan date:2022-5-24 for: VUEN-1095 只读未控制住
|
||||
if (!schema.componentProps.hasOwnProperty('onChange')) {
|
||||
schema.componentProps['onChange'] = (value, formData) => {
|
||||
if (value instanceof Event) {
|
||||
// 输入框 value是event对象
|
||||
value = (value.target as any).value;
|
||||
}
|
||||
// 部门组件抛出事件的value是数组
|
||||
if (value instanceof Array) {
|
||||
value = value.join(',');
|
||||
}
|
||||
// VUEN-1467【vue3 工作流】流程处理 一对多表单 子表tab切换后,关闭不了 导致整个浏览器无法操作 多操作几次,不一定每次必现---
|
||||
if(!this.formRef || !this.formRef.value || !this.formRef.value.$formValueChange){
|
||||
console.log('当前表单无法触发change事件,field:'+this.field)
|
||||
}else{
|
||||
this.formRef.value.$formValueChange(this.field, value, formData)
|
||||
}
|
||||
};
|
||||
// update-begin--author:liaozhiyang---date:20251011---for:【issues/8791】js增强popup弹框的onlChange()没生效
|
||||
if (schema.component === 'JPopup') {
|
||||
schema.componentProps['onPopUpChange'] = schema.componentProps['onChange']
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20251011---for:【issues/8791】js增强popup弹框的onlChange()没生效
|
||||
}
|
||||
}
|
||||
// 顺带处理其他的 schemaProp
|
||||
Object.keys(this.schemaProp).map((k) => {
|
||||
schema[k] = this.schemaProp[k];
|
||||
});
|
||||
}
|
||||
|
||||
noChange() {
|
||||
this.hasChange = false;
|
||||
}
|
||||
|
||||
updateField(field) {
|
||||
this.field = field;
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级查询 没有表单ref对象 手动设置setFieldValue方法用于 popup设置表单值
|
||||
*/
|
||||
setFunctionForFieldValue(func) {
|
||||
if (func) {
|
||||
this.setFieldsValue = func;
|
||||
}
|
||||
}
|
||||
|
||||
asSearchForm() {
|
||||
this.searchForm = true;
|
||||
}
|
||||
|
||||
/**获取modal作为类下拉组件pop的父容器*/
|
||||
getModalAsContainer() {
|
||||
let ele = this.getPopContainer();
|
||||
// update-begin--author:liaozhiyang---date:20231205---for:【QQYUN-7150】online缓存路由打开多页导致下拉类型的组件打不开
|
||||
if (ele != 'body') {
|
||||
const elems = document.querySelectorAll(ele);
|
||||
if (elems && elems.length > 1) {
|
||||
const data: HTMLElement[] = [];
|
||||
elems.forEach((item: HTMLElement) => {
|
||||
if (!(item.offsetWidth == 0 && item.offsetHeight == 0)) {
|
||||
data.push(item);
|
||||
}
|
||||
});
|
||||
if (data.length === 1) {
|
||||
return data[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231205---for:【QQYUN-7150】online缓存路由打开多页导致下拉类型的组件打不开
|
||||
return document.querySelector(ele);
|
||||
}
|
||||
|
||||
/**区分modal表单和查询表单*/
|
||||
getPopContainer() {
|
||||
if (this.searchForm === true) {
|
||||
return 'body';
|
||||
} else if(this.inPopover === true){
|
||||
return `.${this.popContainer}`;
|
||||
}else if(this.popContainer){
|
||||
return `.${this.popContainer} .ant-modal-content`
|
||||
}else {
|
||||
return POP_CONTAINER;
|
||||
}
|
||||
}
|
||||
|
||||
handleWidgetAttr(data) {
|
||||
if (data.ui) {
|
||||
if (data.ui.widgetattrs) {
|
||||
if (data.ui.widgetattrs.disabled == true) {
|
||||
this.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 popContainer
|
||||
*/
|
||||
setCustomPopContainer(modalClass){
|
||||
this.popContainer = modalClass;
|
||||
}
|
||||
|
||||
//update-begin-author:taoyan date:2022-8-5 for: 他表字段/关联记录用
|
||||
// 获取他表字段的 配置信息
|
||||
getLinkFieldInfo():any{
|
||||
return '';
|
||||
}
|
||||
|
||||
// 1.将他表字段的配置信息设置到关联记录字段上
|
||||
setOtherInfo(_arg){
|
||||
}
|
||||
//update-end-author:taoyan date:2022-8-5 for: 他表字段/关联记录用
|
||||
|
||||
// 表单设计器高级查询用
|
||||
isInPopover(){
|
||||
this.inPopover = true;
|
||||
}
|
||||
|
||||
handleDictTableParams() {
|
||||
if (!this.formRef.value) {
|
||||
return
|
||||
}
|
||||
const dictTable = this._data.dictTable as string
|
||||
if (!dictTable) {
|
||||
return
|
||||
}
|
||||
const matches = dictTable.match(/\${([^}]+)}/g)
|
||||
if (!matches || matches.length == 0) {
|
||||
return
|
||||
}
|
||||
// 去除 ${}
|
||||
const keys = matches.map((item: string) => item.replace('${', '').replace('}', ''))
|
||||
const values = computed(() => {
|
||||
const formModel = this.formRef.value.formModel
|
||||
return keys.map((key) => formModel[key]).join('');
|
||||
})
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
watch(values, () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
const formModel = this.formRef.value.formModel
|
||||
// 替换动态参数,如果有 ${xxx} 则替换为实际值
|
||||
let tempDictTable = dictTable.replace(/\${([^}]+)}/g, (_$0, $1) => {
|
||||
if (formModel[$1] == null) {
|
||||
return ''
|
||||
}
|
||||
return formModel[$1]
|
||||
});
|
||||
this.updateDictTable(tempDictTable)
|
||||
}, 150)
|
||||
}, {immediate: true})
|
||||
}
|
||||
|
||||
updateDictTable(_dictTable: string) {
|
||||
console.log('请在子类实现 updateDictTable 方法')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表字典的编码,可替换系统变量
|
||||
* @param dictTable
|
||||
* @param dictText
|
||||
* @param dictCode
|
||||
*/
|
||||
genDictTableCode(dictTable: string, dictText: string, dictCode: string) {
|
||||
// 替换系统变量
|
||||
dictTable = replaceUserInfoByExpression(dictTable)
|
||||
return encodeURI(`${dictTable},${dictText},${dictCode}`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import IFormSchema from '../IFormSchema';
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
|
||||
/**
|
||||
* 省市区
|
||||
*/
|
||||
export default class PcaWidget extends IFormSchema {
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
// update-begin--author:liaozhiyang---date:20260204---for:【QQYUN-14694】online支持配置独立的省、市、县
|
||||
const extendData: any = this.getExtendData();
|
||||
const componentProps: any = {}
|
||||
if (extendData.displayLevel) {
|
||||
componentProps.displayLevel = extendData.displayLevel;
|
||||
componentProps.saveCode = extendData.displayLevel === 'all' ? 'region' : componentProps.displayLevel;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20260204---for:【QQYUN-14694】online支持配置独立的省、市、县
|
||||
return Object.assign({}, item, {
|
||||
component: 'JAreaLinkage',
|
||||
componentProps: {
|
||||
saveCode: 'region',
|
||||
...componentProps,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import IFormSchema from '../IFormSchema';
|
||||
|
||||
/**
|
||||
* 表单设计器-省市区查询
|
||||
*/
|
||||
export default class CascaderPcaForQueryWidget extends IFormSchema {
|
||||
|
||||
schema: Recordable;
|
||||
// 省市县联动级别
|
||||
areaLevel: number;
|
||||
// 是否允许更改级别
|
||||
allowChangeLevel: boolean;
|
||||
|
||||
constructor(key: string, data: Recordable, queryItem: Recordable) {
|
||||
super(key, data);
|
||||
this.schema = data
|
||||
this.areaLevel = data['areaLevel'] ?? 3;
|
||||
// 只有等于和不等于才能更改级别
|
||||
this.allowChangeLevel = ['eq', 'ne'].includes(queryItem?.rule)
|
||||
}
|
||||
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
return Object.assign({}, item, {
|
||||
component: 'CascaderPcaInFilter',
|
||||
componentProps:{
|
||||
areaLevel: this.areaLevel,
|
||||
allowChangeLevel: this.allowChangeLevel,
|
||||
placeholder: '请选择…',
|
||||
style: {
|
||||
width: '100%',
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import IFormSchema from '../IFormSchema';
|
||||
|
||||
/**
|
||||
* checkbox
|
||||
*/
|
||||
export default class CheckboxWidget extends IFormSchema {
|
||||
/*title-value*/
|
||||
options: any[];
|
||||
constructor(key, data) {
|
||||
super(key, data);
|
||||
this.options = this.getOptions(data['enum']);
|
||||
}
|
||||
|
||||
setFormRef(ref) {
|
||||
super.setFormRef(ref);
|
||||
this.handleDictTableParams();
|
||||
}
|
||||
|
||||
updateDictTable(dictTable: string) {
|
||||
this.formRef.value.updateSchema(({
|
||||
field: this.field,
|
||||
componentProps: {
|
||||
options:[],
|
||||
dictCode: this.genDictTableCode(dictTable, this._data.dictText, this._data.dictCode),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
return Object.assign({}, item, {
|
||||
component: 'JCheckbox',
|
||||
componentProps: {
|
||||
options: this.options,
|
||||
triggerChange: true,
|
||||
// update-begin--author:liaozhiyang---date:20230110---for:【QQYUN-7799】字典组件(原生组件除外)加上颜色配置
|
||||
useDicColor: true,
|
||||
// update-end--author:liaozhiyang---date:20230110---for:【QQYUN-7799】字典组件(原生组件除外)加上颜色配置
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getOptions(array) {
|
||||
if (!array || array.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let arr: any[] = [];
|
||||
for (let item of array) {
|
||||
arr.push({
|
||||
value: item.value,
|
||||
label: item.title,
|
||||
// update-begin--author:liaozhiyang---date:20230110---for:【QQYUN-7799】字典组件(原生组件除外)加上颜色配置
|
||||
color: item.color,
|
||||
// update-end--author:liaozhiyang---date:20230110---for:【QQYUN-7799】字典组件(原生组件除外)加上颜色配置
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import IFormSchema from '../IFormSchema';
|
||||
|
||||
enum DateFormat {
|
||||
datetime = 'YYYY-MM-DD HH:mm:ss',
|
||||
date = 'YYYY-MM-DD',
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期、时间
|
||||
*/
|
||||
export default class DateWidget extends IFormSchema {
|
||||
format: string;
|
||||
showTime: boolean;
|
||||
picker: string | undefined;
|
||||
|
||||
allowSelectRange: boolean;
|
||||
|
||||
constructor(key, data, queryItem) {
|
||||
super(key, data);
|
||||
this.format = DateFormat[data.view];
|
||||
this.showTime = data.view == 'date' ? false : true;
|
||||
// update-begin--author:liaozhiyang---date:20240430---for:【issues/6094】online 日期(年月日)控件增加年、年月,年周,年季度等格式
|
||||
let fieldExtendJson = data.fieldExtendJson;
|
||||
if (data.view == 'date' && fieldExtendJson) {
|
||||
fieldExtendJson = JSON.parse(fieldExtendJson);
|
||||
if (fieldExtendJson.picker && fieldExtendJson.picker != 'default') {
|
||||
this.picker = fieldExtendJson.picker;
|
||||
} else {
|
||||
this.picker = undefined;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240430---for:【issues/6094】online 日期(年月日)控件增加年、年月,年周,年季度等格式
|
||||
// 只有等于和不等于才能选择预设范围(今天、昨天、本周等)
|
||||
this.allowSelectRange = ['eq', 'ne'].includes(queryItem?.rule)
|
||||
}
|
||||
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
return Object.assign({}, item, {
|
||||
component: 'DatePickerInFilter',
|
||||
componentProps: {
|
||||
placeholder: `请选择${this.label}`,
|
||||
showTime: this.showTime,
|
||||
valueFormat: this.format,
|
||||
allowSelectRange: this.allowSelectRange,
|
||||
// update-begin--author:liaozhiyang---date:20240430---for:【issues/6094】online 日期(年月日)控件增加年、年月,年周,年季度等格式
|
||||
picker: this.picker,
|
||||
// update-end--author:liaozhiyang---date:20240430---for:【issues/6094】online 日期(年月日)控件增加年、年月,年周,年季度等格式
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
getPopupContainer: (_node) => {
|
||||
return this.getModalAsContainer();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import IFormSchema from '../IFormSchema';
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
|
||||
/**
|
||||
* 富文本
|
||||
*/
|
||||
export default class EditorWidget extends IFormSchema {
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
return Object.assign({}, item, {
|
||||
component: 'JEditor',
|
||||
componentProps: {
|
||||
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1159 第一次加载时,点击第一个输入框,光标会跑到富文本输入框
|
||||
options: {
|
||||
auto_focus: false,
|
||||
},
|
||||
//update-end-author:taoyan date:2022-6-1 for: VUEN-1159 第一次加载时,点击第一个输入框,光标会跑到富文本输入框
|
||||
// fileMax:1,
|
||||
// showImageUpload:false,
|
||||
// width:"966px",
|
||||
// height:"200px"
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import IFormSchema from '../IFormSchema';
|
||||
|
||||
/**
|
||||
* 文件
|
||||
*/
|
||||
export default class FileWidget extends IFormSchema {
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
let componentProps = this.getComponentProps();
|
||||
return Object.assign({}, item, {
|
||||
component: 'JUpload',
|
||||
componentProps,
|
||||
});
|
||||
}
|
||||
|
||||
getComponentProps() {
|
||||
let json = this.getExtendData();
|
||||
if (json && json.uploadnum) {
|
||||
return {
|
||||
maxCount: Number(json.uploadnum),
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import IFormSchema from '../IFormSchema';
|
||||
import { UploadTypeEnum } from '/@/components/Form/src/jeecg/components/JUpload';
|
||||
|
||||
/**
|
||||
* 图片
|
||||
*/
|
||||
export default class ImageWidget extends IFormSchema {
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
let componentProps = this.getComponentProps();
|
||||
return Object.assign({}, item, {
|
||||
component: 'JUpload',
|
||||
componentProps,
|
||||
});
|
||||
}
|
||||
|
||||
getComponentProps() {
|
||||
let props = {
|
||||
fileType: UploadTypeEnum.image,
|
||||
};
|
||||
let json = this.getExtendData();
|
||||
if (json && json.uploadnum) {
|
||||
props['maxCount'] = Number(json.uploadnum);
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import IFormSchema from '../IFormSchema';
|
||||
|
||||
/**
|
||||
* 输入框
|
||||
*/
|
||||
export default class InputWidget extends IFormSchema {
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
if (this.hidden === true) {
|
||||
item['show'] = false;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import IFormSchema from '../IFormSchema';
|
||||
|
||||
/**
|
||||
* 下拉联动- 原理是:
|
||||
* 使用JDictSelectTag组件(2022-03-09测试可行版 后续如有改动请注意)
|
||||
* 监听表单的change事件,清空下级表单值,并改变props
|
||||
* 问题在于1.没有code的时候 不需要设置选项
|
||||
* 优势在于: 可以不考虑组件位置(但是需要改后台接口)
|
||||
*/
|
||||
export default class LinkDownWidget extends IFormSchema {
|
||||
/*title-value*/
|
||||
options: any[];
|
||||
next: string;
|
||||
type: string;
|
||||
table: string;
|
||||
txt: string;
|
||||
store: string;
|
||||
pidField: string;
|
||||
idField: string;
|
||||
origin: boolean;
|
||||
condition: string;
|
||||
|
||||
constructor(key, data) {
|
||||
super(key, data);
|
||||
const { dictTable, dictText, dictCode, pidField, idField, origin, condition } = data;
|
||||
this.table = dictTable;
|
||||
this.txt = dictText;
|
||||
this.store = dictCode;
|
||||
this.idField = idField;
|
||||
this.pidField = pidField;
|
||||
this.origin = origin;
|
||||
this.condition = condition;
|
||||
// 都是空数组
|
||||
this.options = [];
|
||||
this.next = data.next || '';
|
||||
this.type = data.type;
|
||||
}
|
||||
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
let componentProps = this.getComponentProps();
|
||||
return Object.assign({}, item, {
|
||||
component: 'OnlineSelectCascade',
|
||||
componentProps,
|
||||
});
|
||||
}
|
||||
|
||||
getComponentProps() {
|
||||
let baseProp = {
|
||||
table: this.table,
|
||||
txt: this.txt,
|
||||
store: this.store,
|
||||
pidField: this.pidField,
|
||||
idField: this.idField,
|
||||
origin: this.origin,
|
||||
pidValue: '-1',
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
onChange: (value) => {
|
||||
console.log('级联组件-onChange', value);
|
||||
this.valueChange(value);
|
||||
},
|
||||
onNext: (pidValue) => {
|
||||
console.log('级联组件-onNext', pidValue);
|
||||
this.nextOptionsChange(pidValue);
|
||||
},
|
||||
};
|
||||
if (this._data.origin === true) {
|
||||
baseProp['condition'] = this.condition;
|
||||
}
|
||||
return baseProp;
|
||||
}
|
||||
|
||||
async nextOptionsChange(pidValue) {
|
||||
if (!this.formRef) {
|
||||
console.error('表单引用找不到');
|
||||
return;
|
||||
}
|
||||
if (!this.next) {
|
||||
return;
|
||||
}
|
||||
let ref = this.formRef.value;
|
||||
await ref.updateSchema({
|
||||
field: this.next,
|
||||
componentProps: {
|
||||
pidValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async valueChange(value) {
|
||||
if (!this.formRef) {
|
||||
console.error('表单引用找不到');
|
||||
return;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240717---for:【TV360X-1856】联动组件最后一个js增强onchang方法不生效
|
||||
let ref = this.formRef.value;
|
||||
// 触发form层级的change事件
|
||||
ref.$formValueChange(this.field, value);
|
||||
if (this.next) {
|
||||
// 重置value
|
||||
await ref.setFieldsValue({ [this.next]: '' });
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240717---for:【TV360X-1856】联动组件最后一个js增强onchang方法不生效
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import IFormSchema from '../IFormSchema';
|
||||
|
||||
/**
|
||||
* 他表字段
|
||||
*/
|
||||
export default class LinkTableFieldWidget extends IFormSchema {
|
||||
|
||||
dictTable: string;
|
||||
dictText: string;
|
||||
|
||||
constructor(key, data) {
|
||||
super(key, data);
|
||||
this.dictTable = data['dictTable'];
|
||||
this.dictText = data['dictText'];
|
||||
}
|
||||
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
return Object.assign({}, item, {
|
||||
componentProps: {
|
||||
readOnly: true,
|
||||
allowClear: false,
|
||||
disabled: true,
|
||||
style:{
|
||||
background: 'none',
|
||||
color:'rgba(0, 0, 0, 0.85)',
|
||||
border:'none'
|
||||
}
|
||||
}
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取他表字段的关联信息
|
||||
*/
|
||||
getLinkFieldInfo(){
|
||||
let arr = [this.dictTable, `${this.field},${this.dictText}`];
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import IFormSchema from '../IFormSchema';
|
||||
|
||||
/**
|
||||
* 表单设计器-关联记录查询 使用下拉搜索
|
||||
*/
|
||||
export default class LinkTableForQueryWidget extends IFormSchema {
|
||||
|
||||
code: string;
|
||||
titleField: string;
|
||||
multi: boolean;
|
||||
|
||||
constructor(key, data) {
|
||||
super(key, data);
|
||||
this.code = data['code'];
|
||||
this.titleField = data['titleField'];
|
||||
this.multi = data['multi']||false;
|
||||
}
|
||||
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
return Object.assign({}, item, {
|
||||
component: 'LinkTableForQuery',
|
||||
componentProps:{
|
||||
code: this.code,
|
||||
multi: this.multi,
|
||||
field: this.titleField,
|
||||
style: {
|
||||
width: '100%',
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import IFormSchema from '../IFormSchema';
|
||||
|
||||
/**
|
||||
* 关联记录
|
||||
*/
|
||||
export default class LinkTableWidget extends IFormSchema {
|
||||
dictTable: string;
|
||||
dictText: string;
|
||||
dictCode: string;
|
||||
view: string;
|
||||
componentString: string;
|
||||
linkFields: Array<string>;
|
||||
|
||||
constructor(key, data) {
|
||||
super(key, data);
|
||||
this.dictTable = data.dictTable;
|
||||
this.dictText = data.dictText;
|
||||
this.dictCode = data.dictCode;
|
||||
this.view = data.view;
|
||||
this.componentString = ''
|
||||
this.linkFields = []
|
||||
}
|
||||
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
const componentProps = this.getComponentProps()
|
||||
return Object.assign({}, item, {
|
||||
component: this.componentString,
|
||||
componentProps: componentProps
|
||||
});
|
||||
}
|
||||
|
||||
getComponentProps() {
|
||||
let props = {
|
||||
textField: this.dictText,
|
||||
tableName: this.dictTable,
|
||||
valueField: this.dictCode,
|
||||
};
|
||||
let extend = this.getExtendData();
|
||||
// 是否多选
|
||||
if (extend.multiSelect) {
|
||||
props['multi'] = true;
|
||||
}else{
|
||||
props['multi'] = false;
|
||||
}
|
||||
//封面图
|
||||
if (extend.imageField) {
|
||||
props['imageField'] = extend.imageField;
|
||||
}else{
|
||||
props['imageField'] = ''
|
||||
}
|
||||
//显示类型
|
||||
if (extend.showType=='select') {
|
||||
this.componentString = 'LinkTableSelect'
|
||||
let popContainer = this.getPopContainer();
|
||||
props['popContainer'] = popContainer
|
||||
}else{
|
||||
this.componentString = 'LinkTableCard'
|
||||
}
|
||||
if(this.linkFields.length>0){
|
||||
props['linkFields'] = this.linkFields;
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
// 他表字段用于翻译
|
||||
setOtherInfo(arr){
|
||||
// ["表单字段,表字典字段","表单字段,表字典字段"]
|
||||
this.linkFields = arr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import IFormSchema from '../IFormSchema';
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
|
||||
/**
|
||||
* markdown
|
||||
*/
|
||||
export default class MarkdownWidget extends IFormSchema {
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
return Object.assign({}, item, {
|
||||
component: 'JMarkdownEditor',
|
||||
componentProps: {
|
||||
// height: 300,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import IFormSchema from '../IFormSchema';
|
||||
|
||||
/**
|
||||
* 输入框-数字
|
||||
*/
|
||||
export default class NumberWidget extends IFormSchema {
|
||||
dbPointLength: number;
|
||||
|
||||
constructor(key, data) {
|
||||
super(key, data);
|
||||
this.dbPointLength = data.dbPointLength;
|
||||
}
|
||||
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
let componentProps = this.getComponentProps();
|
||||
const safeIntRule = {
|
||||
validator: (_rule, value) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER) {
|
||||
return Promise.reject(`数值超出安全范围(${Number.MIN_SAFE_INTEGER}~${Number.MAX_SAFE_INTEGER}),精度将丢失,请重新输入`);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
const existingRules = item.rules || [];
|
||||
return Object.assign({}, item, {
|
||||
component: 'InputNumber',
|
||||
componentProps,
|
||||
// update-begin--author:liaozhiyang---date:20260413---for:【QQYUN-9790】online中数字类型超出js语言数值范围加提示
|
||||
rules: [...existingRules, safeIntRule],
|
||||
// update-end--author:liaozhiyang---date:20260413---for:【QQYUN-9790】online中数字类型超出js语言数值范围加提示
|
||||
});
|
||||
}
|
||||
|
||||
getComponentProps() {
|
||||
const props = {
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
};
|
||||
if (this.dbPointLength >= 0) {
|
||||
props['precision'] = this.dbPointLength;
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import IFormSchema from '../IFormSchema';
|
||||
|
||||
/**
|
||||
* 输入框- 密码
|
||||
*/
|
||||
export default class PasswordWidget extends IFormSchema {
|
||||
getItem(): FormSchema {
|
||||
let item = super.getItem();
|
||||
return Object.assign({}, item, {
|
||||
component: 'InputPassword',
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user