Merge remote-tracking branch 'refs/remotes/yudao/dev' into dev-crm

pull/483/head
puhui999 2024-07-17 18:04:01 +08:00
commit 1ad5de57f9
27 changed files with 935 additions and 218 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

View File

@ -191,26 +191,24 @@ ps核心功能已经实现正在对接微信小程序中...
### 商城系统
演示地址:<https://doc.iocoder.cn/mall-preview/>
![功能图](/.image/common/mall-feature.png)
![功能图](/.image/common/mall-preview.png)
_前端基于 crmeb uniapp 经过授权重构优化代码实现接入芋道快速开发平台_
演示地址:<https://doc.iocoder.cn/mall-preview/>
### ERP 系统
![功能图](/.image/common/erp-feature.png)
演示地址:<https://doc.iocoder.cn/erp-preview/>
![功能图](/.image/common/erp-feature.png)
### CRM 系统
![功能图](/.image/common/crm-feature.png)
演示地址:<https://doc.iocoder.cn/crm-preview/>
![功能图](/.image/common/crm-feature.png)
## 🐷 演示图
### 系统功能

View File

@ -56,6 +56,10 @@ export const ImageApi = {
getImagePageMy: async (params: PageParam) => {
return await request.get({ url: `/ai/image/my-page`, params })
},
// 获取公开的绘图记录
getImagePagePublic: async (params: PageParam) => {
return await request.get({ url: `/ai/image/public-page`, params })
},
// 获取【我的】绘图记录
getImageMy: async (id: number) => {
return await request.get({ url: `/ai/image/get-my?id=${id}` })

85
src/api/ai/write/index.ts Normal file
View File

@ -0,0 +1,85 @@
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { getAccessToken } from '@/utils/auth'
import { config } from '@/config/axios/config'
import { AiWriteTypeEnum } from '@/views/ai/utils/constants'
import request from '@/config/axios'
export interface WriteVO {
type: AiWriteTypeEnum.WRITING | AiWriteTypeEnum.REPLY // 1:撰写 2:回复
prompt: string // 写作内容提示 1。撰写 2回复
originalContent: string // 原文
length: number // 长度
format: number // 格式
tone: number // 语气
language: number // 语言
userId?: number // 用户编号
platform?: string // 平台
model?: string // 模型
generatedContent?: string // 生成的内容
errorMessage?: string // 错误信息
createTime?: Date // 创建时间
}
export interface AiWritePageReqVO extends PageParam {
userId?: number // 用户编号
type?: AiWriteTypeEnum // 写作类型
platform?: string // 平台
createTime?: [string, string] // 创建时间
}
export interface AiWriteRespVo {
id: number
userId: number
type: number
platform: string
model: string
prompt: string
generatedContent: string
originalContent: string
length: number
format: number
tone: number
language: number
errorMessage: string
createTime: string
}
export const WriteApi = {
writeStream: ({
data,
onClose,
onMessage,
onError,
ctrl
}: {
data: WriteVO
onMessage?: (res: any) => void
onError?: (...args: any[]) => void
onClose?: (...args: any[]) => void
ctrl: AbortController
}) => {
const token = getAccessToken()
return fetchEventSource(`${config.base_url}/ai/write/generate-stream`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
openWhenHidden: true,
body: JSON.stringify(data),
onmessage: onMessage,
onerror: onError,
onclose: onClose,
signal: ctrl.signal
})
},
// 获取写作列表
getWritePage: (params: AiWritePageReqVO) => {
return request.get<PageResult<AiWriteRespVo[]>>({ url: `/ai/write/page`, params })
},
// 删除写作
deleteWrite(id: number) {
return request.delete({ url: `/ai/write/delete`, params: { id } })
}
}

View File

@ -1,43 +0,0 @@
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { getAccessToken } from '@/utils/auth'
import { config } from '@/config/axios/config'
export interface WriteVO {
type: 1 | 2 // 1:撰写 2:回复
prompt: string // 写作内容提示 1。撰写 2回复
originalContent: string // 原文
length: number // 长度
format: number // 格式
tone: number // 语气
language: number // 语言
}
export const writeStream = ({
data,
onClose,
onMessage,
onError,
ctrl
}: {
data: WriteVO
onMessage?: (res: any) => void
onError?: (...args: any[]) => void
onClose?: (...args: any[]) => void
ctrl: AbortController
}) => {
const token = getAccessToken()
return fetchEventSource(`${config.base_url}/ai/write/generate-stream`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
openWhenHidden: true,
body: JSON.stringify(data),
onmessage: onMessage,
onerror: onError,
onclose: onClose,
signal: ctrl.signal
})
}

View File

@ -1,28 +1,36 @@
<template>
<div ref="contentRef" class="markdown-view" v-html="contentHtml"></div>
</template>
<script setup lang="ts">
import {useClipboard} from "@vueuse/core";
import {marked} from 'marked'
import { useClipboard } from '@vueuse/core'
import { marked } from 'marked'
import 'highlight.js/styles/vs2015.min.css'
import hljs from 'highlight.js'
import {ref} from "vue";
const {copy} = useClipboard() // copy
//
const props = defineProps({
content: {
type: String,
required: true
}
})
const message = useMessage() //
const { copy } = useClipboard() // copy
const contentRef = ref()
const contentHtml = ref<any>() // html
const { content } = toRefs(props) // props
// https://highlightjs.org/
// markdownmarked
// marked
/** marked 渲染器 */
const renderer = {
code(code, language, c) {
let highlightHtml
try {
highlightHtml = hljs.highlight(code, {language: language, ignoreIllegals: true}).value
highlightHtml = hljs.highlight(code, { language: language, ignoreIllegals: true }).value
} catch (e) {
// skip
}
@ -36,50 +44,30 @@ marked.use({
renderer: renderer
})
// html
const contentHtml = ref<any>()
//
const props = defineProps({
content: {
type: String,
required: true
}
})
// props
const { content } = toRefs(props)
// content
/** 监听 content 变化 */
watch(content, async (newValue, oldValue) => {
await renderMarkdown(newValue);
await renderMarkdown(newValue)
})
// markdown
/** 渲染 markdown */
const renderMarkdown = async (content: string) => {
contentHtml.value = await marked(content)
}
//
onMounted(async () => {
/** 初始化 **/
onMounted(async () => {
// markdown
await renderMarkdown(props.content as string);
//
await renderMarkdown(props.content as string)
// copy
contentRef.value.addEventListener('click', (e: any) => {
console.log(e)
if (e.target.id === 'copy') {
copy(e.target?.dataset?.copy)
ElMessage({
message: '复制成功!',
type: 'success'
})
message.success('复制成功!')
}
})
})
</script>
<style lang="scss">
.markdown-view {
font-family: PingFang SC;

View File

@ -38,24 +38,24 @@ provide('reload', reload)
:class="[
'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
{
'!h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
(fixedHeader &&
(layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
footer) ||
(!tagsView && layout === 'top' && footer),
'!h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
tagsView && layout === 'top' && footer,
'!h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
!fixedHeader && layout === 'classic' && footer,
'!h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
!fixedHeader && layout === 'topLeft' && footer,
'!h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
fixedHeader && layout === 'cutMenu' && footer,
'!h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
!fixedHeader && layout === 'cutMenu' && footer
}
]"

View File

@ -70,26 +70,26 @@ const remainingRouter: AppRouteRecordRaw[] = [
}
]
},
{
path: '/ai/music',
component: Layout,
redirect: '/index',
name: 'AIMusic',
meta: {},
children: [
{
path: 'index',
component: () => import('@/views/ai/music/components/index.vue'),
name: 'AIMusicIndex',
meta: {
title: 'AI 音乐',
icon: 'ep:home-filled',
noCache: false,
affix: true
}
}
]
},
// {
// path: '/ai/music',
// component: Layout,
// redirect: '/index',
// name: 'AIMusic',
// meta: {},
// children: [
// {
// path: 'index',
// component: () => import('@/views/ai/music/components/index.vue'),
// name: 'AIMusicIndex',
// meta: {
// title: 'AI 音乐',
// icon: 'ep:home-filled',
// noCache: false,
// affix: true
// }
// }
// ]
// },
{
path: '/user',
component: Layout,

View File

@ -222,5 +222,10 @@ export enum DICT_TYPE {
AI_PLATFORM = 'ai_platform', // AI 平台
AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
AI_GENERATE_MODE = 'ai_generate_mode' // AI 生成模式
AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
AI_WRITE_TYPE = 'ai_write_type', // AI 写作类型
AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气
AI_WRITE_LANGUAGE = 'ai_write_language' // AI 写作语言
}

View File

@ -185,9 +185,11 @@ onUnmounted(async () => {
}
})
</script>
<!-- TODO fan 2 scss 可以合并么 -->
<style lang="scss">
.dr-task {
width: 100%;
height: 100%;
}
.task-card {
margin: 0;
padding: 0;
@ -229,9 +231,3 @@ onUnmounted(async () => {
align-items: center;
}
</style>
<style scoped lang="scss">
.dr-task {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,216 @@
<!-- dall3 -->
<template>
<div class="prompt">
<el-text tag="b">画面描述</el-text>
<el-text tag="p">建议使用形容词+动词+风格的格式使用隔开</el-text>
<el-input
v-model="prompt"
maxlength="1024"
rows="5"
class="w-100% mt-15px"
input-style="border-radius: 7px;"
placeholder="例如:童话里的小屋应该是什么样子?"
show-word-limit
type="textarea"
/>
</div>
<div class="hot-words">
<div>
<el-text tag="b">随机热词</el-text>
</div>
<el-space wrap class="word-list">
<el-button
round
class="btn"
:type="selectHotWord === hotWord ? 'primary' : 'default'"
v-for="hotWord in ImageHotWords"
:key="hotWord"
@click="handleHotWordClick(hotWord)"
>
{{ hotWord }}
</el-button>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">平台</el-text>
</div>
<el-space wrap class="group-item-body">
<el-select
v-model="otherPlatform"
placeholder="Select"
size="large"
class="!w-350px"
@change="handlerPlatformChange"
>
<el-option
v-for="item in OtherPlatformEnum"
:key="item.key"
:label="item.name"
:value="item.key"
/>
</el-select>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">模型</el-text>
</div>
<el-space wrap class="group-item-body">
<el-select v-model="model" placeholder="Select" size="large" class="!w-350px">
<el-option v-for="item in models" :key="item.key" :label="item.name" :value="item.key" />
</el-select>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">图片尺寸</el-text>
</div>
<el-space wrap class="group-item-body">
<el-input v-model="width" type="number" class="w-170px" placeholder="图片宽度" />
<el-input v-model="height" type="number" class="w-170px" placeholder="图片高度" />
</el-space>
</div>
<div class="btns">
<el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage">
{{ drawIn ? '生成中' : '生成内容' }}
</el-button>
</div>
</template>
<script setup lang="ts">
import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
import {
AiPlatformEnum,
ChatGlmModels,
ImageHotWords,
ImageModelVO,
OtherPlatformEnum,
QianFanModels,
TongYiWanXiangModels
} from '@/views/ai/utils/constants'
const message = useMessage() //
//
const drawIn = ref<boolean>(false) //
const selectHotWord = ref<string>('') //
//
const prompt = ref<string>('') //
const width = ref<number>(512) //
const height = ref<number>(512) //
const otherPlatform = ref<string>(AiPlatformEnum.TONG_YI) //
const models = ref<ImageModelVO[]>(TongYiWanXiangModels) // TongYiWanXiangModelsQianFanModels
const model = ref<string>(models.value[0].key) //
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // emits
/** 选择热词 */
const handleHotWordClick = async (hotWord: string) => {
//
if (selectHotWord.value == hotWord) {
selectHotWord.value = ''
return
}
//
selectHotWord.value = hotWord //
prompt.value = hotWord //
}
/** 图片生成 */
const handleGenerateImage = async () => {
//
await message.confirm(`确认生成内容?`)
try {
//
drawIn.value = true
//
emits('onDrawStart', AiPlatformEnum.STABLE_DIFFUSION)
//
const form = {
platform: otherPlatform.value,
model: model.value, //
prompt: prompt.value, //
width: width.value, //
height: height.value, //
options: {}
} as unknown as ImageDrawReqVO
await ImageApi.drawImage(form)
} finally {
//
emits('onDrawComplete', AiPlatformEnum.STABLE_DIFFUSION)
//
drawIn.value = false
}
}
/** 填充值 */
const settingValues = async (detail: ImageVO) => {
prompt.value = detail.prompt
width.value = detail.width
height.value = detail.height
}
/** 平台切换 */
const handlerPlatformChange = async (platform: string) => {
//
if (AiPlatformEnum.TONG_YI === platform) {
models.value = TongYiWanXiangModels
} else if (AiPlatformEnum.YI_YAN === platform) {
models.value = QianFanModels
} else if (AiPlatformEnum.ZHI_PU === platform) {
models.value = ChatGlmModels
} else {
models.value = []
}
//
if (models.value.length > 0) {
model.value = models.value[0].key
} else {
model.value = ''
}
}
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>
<style scoped lang="scss">
//
.prompt {
}
//
.hot-words {
display: flex;
flex-direction: column;
margin-top: 30px;
.word-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
margin-top: 15px;
.btn {
margin: 0;
}
}
}
//
.group-item {
margin-top: 30px;
.group-item-body {
margin-top: 15px;
width: 100%;
}
}
.btns {
display: flex;
justify-content: center;
margin-top: 50px;
}
</style>

View File

@ -18,6 +18,11 @@
ref="stableDiffusionRef"
@on-draw-complete="handleDrawComplete"
/>
<Other
v-if="selectPlatform === 'other'"
ref="otherRef"
@on-draw-complete="handleDrawComplete"
/>
</div>
</div>
<div class="main">
@ -33,11 +38,13 @@ import { ImageVO } from '@/api/ai/image'
import Dall3 from './components/dall3/index.vue'
import Midjourney from './components/midjourney/index.vue'
import StableDiffusion from './components/stableDiffusion/index.vue'
import Other from './components/other/index.vue'
const imageListRef = ref<any>() // image ref
const dall3Ref = ref<any>() // dall3(openai) ref
const midjourneyRef = ref<any>() // midjourney ref
const stableDiffusionRef = ref<any>() // stable diffusion ref
const otherRef = ref<any>() // stable diffusion ref
//
const selectPlatform = ref(AiPlatformEnum.MIDJOURNEY)
@ -53,6 +60,10 @@ const platformOptions = [
{
label: 'Stable Diffusion',
value: AiPlatformEnum.STABLE_DIFFUSION
},
{
label: '其它',
value: 'other'
}
]
@ -77,6 +88,7 @@ const handleRegeneration = async (image: ImageVO) => {
} else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
stableDiffusionRef.value.settingValues(image)
}
// TODO @fan other
}
</script>

View File

@ -0,0 +1,62 @@
<template>
<div class="gallery">
<div v-for="item in publicList" :key="item" class="gallery-item">
<img :src="item.picUrl" class="img"/>
</div>
</div>
</template>
<script setup lang="ts">
import { ImageApi, ImageVO, ImageMidjourneyButtonsVO } from '@/api/ai/image'
/** 属性 */
const pageNo = ref<number>(1)
const pageSize = ref<number>(20)
const publicList = ref<ImageVO[]>([])
/** 获取数据 */
const getListData = async () => {
const res = await ImageApi.getImagePagePublic({pageNo: pageNo.value, pageSize: pageSize.value});
publicList.value = res.list as ImageVO[];
console.log('publicList.value', publicList.value)
}
onMounted(async () => {
await getListData()
})
</script>
<style scoped lang="scss">
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
//max-width: 1000px;
padding: 20px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.gallery-item {
position: relative;
overflow: hidden;
background: #f0f0f0;
cursor: pointer;
transition: transform 0.3s;
}
.gallery-item img {
width: 100%;
height: auto;
display: block;
transition: transform 0.3s;
}
.gallery-item:hover img {
transform: scale(1.1);
}
.gallery-item:hover {
transform: scale(1.05);
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="flex h-1/1">
<div class="flex ">
<!-- 模式 -->
<Mode class="flex-none" @generate-music="generateMusic"/>
<!-- 音频列表 -->
@ -13,7 +13,7 @@ import List from './list/index.vue'
defineOptions({ name: 'Index' })
const listRef = ref<{generateMusic: (...args) => void} | null>(null)
const listRef = ref<Nullable<{generateMusic: (...args) => void}>>(null)
function generateMusic (args: {formData: Recordable}) {
unref(listRef)?.generateMusic(args.formData)

View File

@ -1,9 +1,68 @@
<template>
<div class="h-72px bg-[var(--el-bg-color-overlay)] b-solid b-1 b-[var(--el-border-color)] b-l-none">播放器</div>
<div class="flex items-center justify-between px-2 h-72px bg-[var(--el-bg-color-overlay)] b-solid b-1 b-[var(--el-border-color)] b-l-none">
<!-- 歌曲信息 -->
<div class="flex gap-[10px]">
<el-image src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" class="w-[45px]"/>
<div>
<div>我很好</div>
<div class="text-[12px] text-gray-400">刘大壮</div>
</div>
</div>
<!-- 音频controls -->
<div class="flex gap-[12px] items-center">
<Icon icon="majesticons:back-circle" :size="20" class="text-gray-300 cursor-pointer"/>
<Icon :icon="audioProps.paused ? 'mdi:arrow-right-drop-circle' : 'solar:pause-circle-bold'" :size="30" class=" cursor-pointer" @click="toggleStatus('paused')"/>
<Icon icon="majesticons:next-circle" :size="20" class="text-gray-300 cursor-pointer"/>
<div class="flex gap-[16px] items-center">
<span>{{audioProps.currentTime}}</span>
<el-slider v-model="audioProps.duration" color="#409eff" class="w-[160px!important] "/>
<span>{{ audioProps.duration }}</span>
</div>
<!-- 音频 -->
<audio v-bind="audioProps" ref="audioRef" controls v-show="!audioProps" @timeupdate="audioTimeUpdate">
<source :src="response"/>
</audio>
</div>
<!-- 音量控制器 -->
<div class="flex gap-[16px] items-center">
<Icon :icon="audioProps.muted ? 'tabler:volume-off' : 'tabler:volume'" :size="20" class="cursor-pointer" @click="toggleStatus('muted')"/>
<el-slider v-model="audioProps.volume" color="#409eff" class="w-[160px!important] "/>
</div>
</div>
</template>
<script lang="ts" setup>
import { formatPast } from '@/utils/formatTime'
import response from '@/assets/audio/response.mp3'
defineOptions({ name: 'Index' })
const audioRef = ref<Nullable<HTMLElement>>(null)
// https://www.runoob.com/tags/ref-av-dom.html
const audioProps = reactive({
autoplay: true,
paused: false,
currentTime: '00:00',
duration: '00:00',
muted: false,
volume: 50,
})
function toggleStatus (type: string) {
audioProps[type] = !audioProps[type]
if (type === 'paused' && audioRef.value) {
if (audioProps[type]) {
audioRef.value.pause()
} else {
audioRef.value.play()
}
}
}
//
function audioTimeUpdate (args) {
audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss')
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="flex flex-col h-full">
<div class="flex flex-col h-[600px]">
<div class="flex-auto flex overflow-hidden">
<el-tabs v-model="currentType" class="flex-auto px-[var(--app-content-padding)]">
<!-- 我的创作 -->

View File

@ -1,5 +1,5 @@
<template>
<ContentWrap class="w-300px h-full">
<ContentWrap class="w-300px h-full mb-[0!important]">
<el-radio-group v-model="generateMode" class="mb-15px">
<el-radio-button label="desc">
描述模式
@ -28,10 +28,7 @@ const emits = defineEmits(['generate-music'])
const generateMode = ref('lyric')
interface ModeRef {
formData: Recordable
}
const modeRef = ref<ModeRef | null>(null)
const modeRef = ref<Nullable<{ formData: Recordable }>>(null)
/*
*@Description: 根据信息生成音乐

View File

@ -9,13 +9,19 @@
label-width="68px"
>
<el-form-item label="用户编号" prop="userId">
<el-input
<el-select
v-model="queryParams.userId"
placeholder="请输入用户编号"
clearable
@keyup.enter="handleQuery"
placeholder="请输入用户编号"
class="!w-240px"
/>
>
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="音乐名称" prop="title">
<el-input

View File

@ -23,6 +23,21 @@ export const AiPlatformEnum = {
SUNO: 'Suno' // Suno AI
}
export const OtherPlatformEnum: ImageModelVO[] = [
{
key: AiPlatformEnum.TONG_YI,
name: '通义万相'
},
{
key: AiPlatformEnum.YI_YAN,
name: '百度千帆'
},
{
key: AiPlatformEnum.ZHI_PU,
name: '智谱 AI'
}
]
/**
* AI
*/
@ -49,6 +64,12 @@ export enum AiWriteTypeEnum {
REPLY // 回复
}
// 表格展示对照map
export const AiWriteTypeTableRender = {
[AiWriteTypeEnum.WRITING]: '撰写',
[AiWriteTypeEnum.REPLY]: '回复',
}
// ========== 【图片 UI】相关的枚举 ==========
export const ImageHotWords = [
'中国旗袍',
@ -189,6 +210,31 @@ export const StableDiffusionStylePresets: ImageModelVO[] = [
}
]
export const TongYiWanXiangModels: ImageModelVO[] = [
{
key: 'wanx-v1',
name: 'wanx-v1'
},
{
key: 'wanx-sketch-to-image-v1',
name: 'wanx-sketch-to-image-v1'
}
]
export const QianFanModels: ImageModelVO[] = [
{
key: 'sd_xl',
name: 'sd_xl'
}
]
export const ChatGlmModels: ImageModelVO[] = [
{
key: 'cogview-3',
name: 'cogview-3'
}
]
export const StableDiffusionClipGuidancePresets: ImageModelVO[] = [
{
key: 'NONE',
@ -248,7 +294,7 @@ export const Dall3StyleList: ImageModelVO[] = [
export interface ImageSizeVO {
key: string
name: string
name?: string
style: string
width: string
height: string
@ -353,3 +399,18 @@ export const NijiVersionList = [
label: 'v5'
}
]
// ========== 【写作 UI】相关的枚举 ==========
/** 写作点击示例时的数据 **/
export const WriteExample = {
write: {
prompt: 'vue',
data: 'Vue.js 是一种用于构建用户界面的渐进式 JavaScript 框架。它的核心库只关注视图层,易于上手,同时也便于与其他库或已有项目整合。\n\nVue.js 的特点包括:\n- 响应式的数据绑定Vue.js 会自动将数据与 DOM 同步,使得状态管理变得更加简单。\n- 组件化Vue.js 允许开发者通过小型、独立和通常可复用的组件构建大型应用。\n- 虚拟 DOMVue.js 使用虚拟 DOM 实现快速渲染,提高了性能。\n\n在 Vue.js 中,一个典型的应用结构可能包括:\n1. 根实例:每个 Vue 应用都需要一个根实例作为入口点。\n2. 组件系统:可以创建自定义的可复用组件。\n3. 指令:特殊的带有前缀 v- 的属性,为 DOM 元素提供特殊的行为。\n4. 插值:用于文本内容,将数据动态地插入到 HTML。\n5. 计算属性和侦听器:用于处理数据的复杂逻辑和响应数据变化。\n6. 条件渲染:根据条件决定元素的渲染。\n7. 列表渲染:用于显示列表数据。\n8. 事件处理:响应用户交互。\n9. 表单输入绑定:处理表单输入和验证。\n10. 组件生命周期钩子:在组件的不同阶段执行特定的函数。\n\nVue.js 还提供了官方的路由器 Vue Router 和状态管理库 Vuex以支持构建复杂的单页应用SPA。\n\n在开发过程中开发者通常会使用 Vue CLI这是一个强大的命令行工具用于快速生成 Vue 项目脚手架,集成了诸如 Babel、Webpack 等现代前端工具,以及热重载、代码检测等开发体验优化功能。\n\nVue.js 的生态系统还包括大量的第三方库和插件,如 VuetifyUI 组件库、Vue Test Utils测试工具这些都极大地丰富了 Vue.js 的开发生态。\n\n总的来说Vue.js 是一个灵活、高效的前端框架,适合从小型项目到大型企业级应用的开发。它的易用性、灵活性和强大的社区支持使其成为许多开发者的首选框架之一。'
},
reply: {
originalContent: '领导,我想请假',
prompt: '不批',
data: '您的请假申请已收悉,经核实和考虑,暂时无法批准您的请假申请。\n\n如有特殊情况或紧急事务请及时与我联系。\n\n祝工作顺利。\n\n谢谢。'
}
}

View File

@ -11,16 +11,3 @@
export const hasChinese = (str: string) => {
return /[\u4e00-\u9fa5]/.test(str)
}
/** 写作点击示例时的数据 **/
export const WriteExampleDataJson = {
write: {
prompt: 'vue',
data: 'Vue.js 是一种用于构建用户界面的渐进式 JavaScript 框架。它的核心库只关注视图层,易于上手,同时也便于与其他库或已有项目整合。\n\nVue.js 的特点包括:\n- 响应式的数据绑定Vue.js 会自动将数据与 DOM 同步,使得状态管理变得更加简单。\n- 组件化Vue.js 允许开发者通过小型、独立和通常可复用的组件构建大型应用。\n- 虚拟 DOMVue.js 使用虚拟 DOM 实现快速渲染,提高了性能。\n\n在 Vue.js 中,一个典型的应用结构可能包括:\n1. 根实例:每个 Vue 应用都需要一个根实例作为入口点。\n2. 组件系统:可以创建自定义的可复用组件。\n3. 指令:特殊的带有前缀 v- 的属性,为 DOM 元素提供特殊的行为。\n4. 插值:用于文本内容,将数据动态地插入到 HTML。\n5. 计算属性和侦听器:用于处理数据的复杂逻辑和响应数据变化。\n6. 条件渲染:根据条件决定元素的渲染。\n7. 列表渲染:用于显示列表数据。\n8. 事件处理:响应用户交互。\n9. 表单输入绑定:处理表单输入和验证。\n10. 组件生命周期钩子:在组件的不同阶段执行特定的函数。\n\nVue.js 还提供了官方的路由器 Vue Router 和状态管理库 Vuex以支持构建复杂的单页应用SPA。\n\n在开发过程中开发者通常会使用 Vue CLI这是一个强大的命令行工具用于快速生成 Vue 项目脚手架,集成了诸如 Babel、Webpack 等现代前端工具,以及热重载、代码检测等开发体验优化功能。\n\nVue.js 的生态系统还包括大量的第三方库和插件,如 VuetifyUI 组件库、Vue Test Utils测试工具这些都极大地丰富了 Vue.js 的开发生态。\n\n总的来说Vue.js 是一个灵活、高效的前端框架,适合从小型项目到大型企业级应用的开发。它的易用性、灵活性和强大的社区支持使其成为许多开发者的首选框架之一。'
},
reply: {
originalContent: '领导,我想请假',
prompt: '不批',
data: '您的请假申请已收悉,经核实和考虑,暂时无法批准您的请假申请。\n\n如有特殊情况或紧急事务请及时与我联系。\n\n祝工作顺利。\n\n谢谢。'
}
}

View File

@ -24,27 +24,28 @@
</h3>
</DefineLabel>
<!-- TODO @hhhero 小屏幕的时候是定位在左边的大屏是分开的 -->
<div class="relative" v-bind="$attrs">
<div class="flex flex-col" v-bind="$attrs">
<!-- tab -->
<div
class="absolute left-1/2 top-2 -translate-x-1/2 w-[303px] rounded-full bg-[#DDDFE3] p-1 z-10"
>
<div
class="flex items-center relative after:content-[''] after:block after:bg-white after:h-[30px] after:w-1/2 after:absolute after:top-0 after:left-0 after:transition-transform after:rounded-full"
:class="selectedTab === AiWriteTypeEnum.REPLY && 'after:transform after:translate-x-[100%]'"
>
<ReuseTab
v-for="tab in tabs"
:key="tab.value"
:text="tab.text"
:active="tab.value === selectedTab"
:itemClick="() => switchTab(tab.value)"
/>
<div class="w-full pt-2 bg-[#f5f7f9] flex justify-center">
<div class="w-[303px] rounded-full bg-[#DDDFE3] p-1 z-10">
<div
class="flex items-center relative after:content-[''] after:block after:bg-white after:h-[30px] after:w-1/2 after:absolute after:top-0 after:left-0 after:transition-transform after:rounded-full"
:class="
selectedTab === AiWriteTypeEnum.REPLY && 'after:transform after:translate-x-[100%]'
"
>
<ReuseTab
v-for="tab in tabs"
:key="tab.value"
:text="tab.text"
:active="tab.value === selectedTab"
:itemClick="() => switchTab(tab.value)"
/>
</div>
</div>
</div>
<div
class="px-7 pb-2 pt-[46px] overflow-y-auto lg:block w-[380px] box-border bg-[#ECEDEF] h-full"
class="px-7 pb-2 flex-grow overflow-y-auto lg:block w-[380px] box-border bg-[#f5f7f9] h-full"
>
<div>
<template v-if="selectedTab === 1">
@ -82,13 +83,13 @@
</template>
<ReuseLabel label="长度" />
<Tag v-model="formData.length" :tags="getIntDictOptions('ai_write_length')" />
<Tag v-model="formData.length" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LENGTH)" />
<ReuseLabel label="格式" />
<Tag v-model="formData.format" :tags="getIntDictOptions('ai_write_format')" />
<Tag v-model="formData.format" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_FORMAT)" />
<ReuseLabel label="语气" />
<Tag v-model="formData.tone" :tags="getIntDictOptions('ai_write_tone')" />
<Tag v-model="formData.tone" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_TONE)" />
<ReuseLabel label="语言" />
<Tag v-model="formData.language" :tags="getIntDictOptions('ai_write_language')" />
<Tag v-model="formData.language" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LANGUAGE)" />
<div class="flex items-center justify-center mt-3">
<el-button :disabled="isWriting" @click="reset"></el-button>
@ -103,15 +104,14 @@
import { createReusableTemplate } from '@vueuse/core'
import { ref } from 'vue'
import Tag from './Tag.vue'
import { WriteVO } from '@/api/ai/writer'
import { WriteVO } from 'src/api/ai/write'
import { omit } from 'lodash-es'
import { getIntDictOptions } from '@/utils/dict'
import { WriteExampleDataJson } from '@/views/ai/utils/utils'
import { AiWriteTypeEnum } from "@/views/ai/utils/constants";
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { AiWriteTypeEnum, WriteExample } from '@/views/ai/utils/constants'
type TabType = WriteVO['type']
const message = useMessage()
const message = useMessage() //
defineProps<{
isWriting: boolean
@ -127,15 +127,17 @@ const emits = defineEmits<{
const example = (type: 'write' | 'reply') => {
formData.value = {
...initData,
...omit(WriteExampleDataJson[type], ['data'])
...omit(WriteExample[type], ['data'])
}
emits('example', type)
}
/** 重置,将表单值作为初选值 **/
const reset = () => {
formData.value = {...initData}
formData.value = { ...initData }
emits('reset')
}
const selectedTab = ref<TabType>(AiWriteTypeEnum.WRITING)
const tabs: {
text: string
@ -151,10 +153,12 @@ const [DefineTab, ReuseTab] = createReusableTemplate<{
}>()
/**
* 可以在template里边定义可复用的组件DefineLabelReuseLabel是采用的解构赋值都是Vue组件
* 直接通过组件的形式使用<DefineLabel v-slot="{ label, hint, hintClick }"></DefineLabel><ReuseLabel />来使用定义的组件
* DefineLabel里边的v-slot="{ label, hint, hintClick }“相当于是解构了组件的prop需要注意的是boolean类型需要显式的赋值比如 <ReuseLabel :flag="true" />
* 事件也得以prop形式传入不能是@event的形式比如下面的hintClick需要<ReuseLabel :hintClick="() => { doSomething }"/>
* 可以在 template 里边定义可复用的组件DefineLabelReuseLabel 是采用的解构赋值都是 Vue 组件
*
* 直接通过组件的形式使用<DefineLabel v-slot="{ label, hint, hintClick }"> <DefineLabel />通过 <ReuseLabel /> 来使用定义的组件
* DefineLabel 里边的 v-slot="{ label, hint, hintClick }"相当于是解构了组件的 prop需要注意的是 boolean 类型需要显式的赋值比如 <ReuseLabel :flag="true" />
* 事件也得以 prop 形式传入不能是 @event的形式比如下面的 hintClick 需要<ReuseLabel :hintClick="() => { doSomething }"/>
*
* @see https://vueuse.org/createReusableTemplate
*/
const [DefineLabel, ReuseLabel] = createReusableTemplate<{
@ -174,12 +178,22 @@ const initData: WriteVO = {
format: 1
}
const formData = ref<WriteVO>({ ...initData })
/** 用来记录切换之前所填写的数据,切换的时候给赋值回来 **/
const recordFormData = {} as Record<AiWriteTypeEnum, WriteVO>
/** 切换tab **/
const switchTab = (value: TabType) => {
selectedTab.value = value
formData.value = { ...initData }
if (value !== selectedTab.value) {
//
recordFormData[selectedTab.value] = formData.value
selectedTab.value = value
//
formData.value = { ...initData, ...recordFormData[value] }
}
}
/** 提交写作 */
const submit = () => {
if (selectedTab.value === 2 && !formData.value.originalContent) {
message.warning('请输入原文')
@ -192,7 +206,7 @@ const submit = () => {
emits('submit', {
/** 撰写的时候没有 originalContent 字段**/
...(selectedTab.value === 1 ? omit(formData.value, ['originalContent']) : formData.value),
/** 使用选中tab值覆盖当前的type类型 **/
/** 使用选中 tab 值覆盖当前的 type 类型 **/
type: selectedTab.value
})
}

View File

@ -1,24 +1,19 @@
<template>
<div class="h-full box-border flex flex-col px-7">
<h3 class="m-0 h-14 -mx-7 px-7 shrink-0 flex items-center justify-between bg-[#ecedef]">
<span>预览</span>
<!-- 展示在右上角 -->
<el-button
color="#846af7"
v-show="showCopy"
@click="copyContent"
size="small"
>
<template #icon>
<Icon icon="ph:copy-bold" />
</template>
复制
</el-button>
<el-card class="my-card h-full">
<template #header>
<h3 class="m-0 px-7 shrink-0 flex items-center justify-between">
<span>预览</span>
<!-- 展示在右上角 -->
<el-button color="#846af7" v-show="showCopy" @click="copyContent" size="small">
<template #icon>
<Icon icon="ph:copy-bold" />
</template>
复制
</el-button>
</h3>
</template>
</h3>
<div ref="contentRef" class="hide-scroll-bar flex-grow box-border overflow-y-auto ">
<div ref="contentRef" class="hide-scroll-bar h-full box-border overflow-y-auto">
<div class="w-full min-h-full relative flex-grow bg-white box-border p-3 sm:p-7">
<!-- 终止生成内容的按钮 -->
<el-button
@ -43,14 +38,14 @@
/>
</div>
</div>
</div>
</el-card>
</template>
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'
const message = useMessage()
const { copied, copy } = useClipboard()
const message = useMessage() //
const { copied, copy } = useClipboard() //
const props = defineProps({
content: {
@ -67,7 +62,7 @@ const props = defineProps({
const emits = defineEmits(['update:content', 'stopStream'])
//
/** 通过计算属性,双向绑定,更改生成的内容,考虑到用户想要更改生成文章的情况 */
const compContent = computed({
get() {
return props.content
@ -91,7 +86,7 @@ const copyContent = () => {
copy(props.content)
}
// copied.valuetrue
/** 复制成功的时候 copied.value 为 true */
watch(copied, (val) => {
if (val) {
message.success('复制成功')
@ -109,4 +104,17 @@ watch(copied, (val) => {
height: 0;
}
}
.my-card {
display: flex;
flex-direction: column;
:deep(.el-card__body) {
box-sizing: border-box;
flex-grow: 1;
overflow-y: auto;
padding: 0;
@extend .hide-scroll-bar;
}
}
</style>

View File

@ -1,3 +1,4 @@
<!-- 标签选项 -->
<template>
<div class="flex flex-wrap gap-[8px]">
<span

View File

@ -1,5 +1,5 @@
<template>
<div class="h-[calc(100vh-var(--top-tool-height)-var(--app-footer-height)-40px)] -m-5 flex">
<div class="absolute top-0 left-0 right-0 bottom-0 flex">
<Left
:is-writing="isWriting"
class="h-full"
@ -18,10 +18,10 @@
</template>
<script setup lang="ts">
import Left from '../components/Left.vue'
import Right from '../components/Right.vue'
import * as WriteApi from '@/api/ai/writer'
import { WriteExampleDataJson } from '@/views/ai/utils/utils'
import Left from './components/Left.vue'
import Right from './components/Right.vue'
import { WriteApi, WriteVO } from '@/api/ai/write'
import { WriteExample } from '@/views/ai/utils/constants'
const message = useMessage()
@ -37,7 +37,7 @@ const stopStream = () => {
/** 执行写作 */
const rightRef = ref<InstanceType<typeof Right>>()
const submit = (data) => {
const submit = (data: WriteVO) => {
abortController.value = new AbortController()
writeResult.value = ''
isWriting.value = true
@ -51,9 +51,9 @@ const submit = (data) => {
return
}
writeResult.value = writeResult.value + data
nextTick(() => {
rightRef.value?.scrollToBottom()
})
//
await nextTick()
rightRef.value?.scrollToBottom()
},
ctrl: abortController.value,
onClose: stopStream,
@ -65,8 +65,8 @@ const submit = (data) => {
}
/** 点击示例触发 */
const handleExampleClick = (type: keyof typeof WriteExampleDataJson) => {
writeResult.value = WriteExampleDataJson[type].data
const handleExampleClick = (type: keyof typeof WriteExample) => {
writeResult.value = WriteExample[type].data
}
/** 点击重置的时候清空写作的结果**/

View File

@ -0,0 +1,256 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="用户编号" prop="userId">
<el-select
v-model="queryParams.userId"
clearable
placeholder="请输入用户编号"
class="!w-240px"
>
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="写作类型" prop="type">
<el-select
v-model="queryParams.type"
placeholder="请选择写作类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.AI_WRITE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="平台" prop="platform">
<el-select v-model="queryParams.platform" placeholder="请选择平台" clearable class="!w-240px">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['ai:write:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<!-- TODO @YunaiV 目前没有导出接口需要导出吗 -->
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['ai:write:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="编号" align="center" prop="id" width="120" fixed="left" />
<el-table-column label="用户" align="center" prop="userId" width="180">
<template #default="scope">
<span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
</template>
</el-table-column>
<el-table-column label="写作类型" align="center" prop="type">
<template #default="scope">
<dict-tag :type="DICT_TYPE.AI_WRITE_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="平台" align="center" prop="platform" width="120">
<template #default="scope">
<dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
</template>
</el-table-column>
<el-table-column label="模型" align="center" prop="model" width="180" />
<el-table-column
label="生成内容提示"
align="center"
prop="prompt"
width="180"
show-overflow-tooltip
/>
<el-table-column label="生成的内容" align="center" prop="generatedContent" width="180" />
<el-table-column label="原文" align="center" prop="originalContent" width="180" />
<el-table-column label="长度" align="center" prop="length">
<template #default="scope">
<dict-tag :type="DICT_TYPE.AI_WRITE_LENGTH" :value="scope.row.length" />
</template>
</el-table-column>
<el-table-column label="格式" align="center" prop="format">
<template #default="scope">
<dict-tag :type="DICT_TYPE.AI_WRITE_FORMAT" :value="scope.row.format" />
</template>
</el-table-column>
<el-table-column label="语气" align="center" prop="tone">
<template #default="scope">
<dict-tag :type="DICT_TYPE.AI_WRITE_TONE" :value="scope.row.tone" />
</template>
</el-table-column>
<el-table-column label="语言" align="center" prop="language">
<template #default="scope">
<dict-tag :type="DICT_TYPE.AI_WRITE_LANGUAGE" :value="scope.row.language" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="错误信息" align="center" prop="errorMessage" />
<el-table-column label="操作" align="center">
<template #default="scope">
<!-- TODO @YunaiV 目前没有修改接口写作要可以更改吗-->
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['ai:write:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['ai:write:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { useRouter } from 'vue-router'
import { WriteApi, AiWritePageReqVO, AiWriteRespVo } from '@/api/ai/write'
import * as UserApi from '@/api/system/user'
/** AI 写作列表 */
defineOptions({ name: 'AiWriteManager' })
const message = useMessage() //
const { t } = useI18n() //
const router = useRouter() //
const loading = ref(true) //
const list = ref<AiWriteRespVo[]>([]) //
const total = ref(0) //
const queryParams = reactive<AiWritePageReqVO>({
pageNo: 1,
pageSize: 10,
userId: undefined,
type: undefined,
platform: undefined,
createTime: undefined
})
const queryFormRef = ref() //
const userList = ref<UserApi.UserVO[]>([]) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await WriteApi.getWritePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 新增方法,跳转到写作页面 **/
const openForm = (type: string, id?: number) => {
switch (type) {
case 'create':
router.push('/ai/write')
break
}
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await WriteApi.deleteWrite(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
getList()
//
userList.value = await UserApi.getSimpleUserList()
})
</script>

5
types/global.d.ts vendored
View File

@ -50,4 +50,9 @@ declare global {
name: string
children?: Tree[] | any[]
}
// 分页数据公共返回
interface PageResult<T> {
list: T // 数据
total: number // 总量
}
}