mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	merge app type function branch
This commit is contained in:
		@@ -3,7 +3,7 @@
 | 
			
		||||
* 功能优化:用户文件列表组件增加分页功能支持
 | 
			
		||||
* Bug修复:修复用户注册失败Bug,注册操作只弹出一次行为验证码
 | 
			
		||||
* 功能优化:首次登录不需要验证码,直接登录,登录失败之后才弹出验证码
 | 
			
		||||
* 功能新增:给 AI 应用(角色)增加分类
 | 
			
		||||
* 功能新增:给 AI 应用(角色)增加分类,前端支持分类筛选
 | 
			
		||||
* 功能优化:允许用户在聊天页面设置是否使用流式输出或者一次性输出,兼容 GPT-O1 模型。
 | 
			
		||||
 | 
			
		||||
## v4.1.3
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,8 @@ html, body {
 | 
			
		||||
    margin 0;
 | 
			
		||||
 | 
			
		||||
    .el-dialog__body {
 | 
			
		||||
      max-height 90vh
 | 
			
		||||
      max-height 80vh
 | 
			
		||||
      overflow-y auto
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,14 +2,54 @@
 | 
			
		||||
  background-color: #282c34;
 | 
			
		||||
  height 100%
 | 
			
		||||
 | 
			
		||||
  .inner {
 | 
			
		||||
  .apps-type-nav{
 | 
			
		||||
    height 43px
 | 
			
		||||
    padding 8px 0;
 | 
			
		||||
    margin-bottom 3px
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .scrollbar-type-nav{
 | 
			
		||||
    display flex
 | 
			
		||||
    align-items center
 | 
			
		||||
    height 43px
 | 
			
		||||
    padding 0 5px
 | 
			
		||||
    li{
 | 
			
		||||
      flex-shrink 0
 | 
			
		||||
      display flex
 | 
			
		||||
      align-items center
 | 
			
		||||
      justify-content center
 | 
			
		||||
      margin 0 10px
 | 
			
		||||
      height 26px
 | 
			
		||||
      border-radius 4px
 | 
			
		||||
      border 1px solid rgb(80,80,80)
 | 
			
		||||
      padding 2px 12px
 | 
			
		||||
      background rgba(60,60,60 0.9)
 | 
			
		||||
      color #fff
 | 
			
		||||
      font-size 14px
 | 
			
		||||
      cursor pointer
 | 
			
		||||
 | 
			
		||||
      .image {
 | 
			
		||||
        width 22px
 | 
			
		||||
        height 22px
 | 
			
		||||
        overflow hidden
 | 
			
		||||
        margin-right 5px
 | 
			
		||||
        border-radius 50%
 | 
			
		||||
      }
 | 
			
		||||
      &.active{
 | 
			
		||||
        background #21aa93;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  .app-list-container {
 | 
			
		||||
    display flex
 | 
			
		||||
    color #ffffff
 | 
			
		||||
    padding 15px;
 | 
			
		||||
    padding 2px 15px;
 | 
			
		||||
    overflow-y visible
 | 
			
		||||
    overflow-x hidden
 | 
			
		||||
 | 
			
		||||
    .list-box {
 | 
			
		||||
    .item__list-box {
 | 
			
		||||
      .item {
 | 
			
		||||
        display flex
 | 
			
		||||
        flex-flow row
 | 
			
		||||
 
 | 
			
		||||
@@ -141,7 +141,7 @@
 | 
			
		||||
        display flex
 | 
			
		||||
        flex-flow row
 | 
			
		||||
        align-items center
 | 
			
		||||
        height 100px
 | 
			
		||||
        min-height 100px
 | 
			
		||||
        padding 10px 15px
 | 
			
		||||
        border-radius 10px
 | 
			
		||||
        cursor pointer
 | 
			
		||||
 
 | 
			
		||||
@@ -1,57 +1,64 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-container class="file-select-box">
 | 
			
		||||
    <a class="file-upload-img" @click="fetchFiles">
 | 
			
		||||
    <a class="file-upload-img" @click="fetchFiles(1)">
 | 
			
		||||
      <i class="iconfont icon-attachment-st"></i>
 | 
			
		||||
    </a>
 | 
			
		||||
    <el-dialog
 | 
			
		||||
        class="file-list-dialog"
 | 
			
		||||
        v-model="show"
 | 
			
		||||
        :close-on-click-modal="true"
 | 
			
		||||
        :show-close="true"
 | 
			
		||||
        :width="800"
 | 
			
		||||
        title="文件管理"
 | 
			
		||||
    >
 | 
			
		||||
 | 
			
		||||
      <div class="file-list">
 | 
			
		||||
        <el-row :gutter="20">
 | 
			
		||||
          <el-col :span="3">
 | 
			
		||||
            <div class="grid-content">
 | 
			
		||||
              <el-upload
 | 
			
		||||
                  class="avatar-uploader"
 | 
			
		||||
                  :auto-upload="true"
 | 
			
		||||
                  :show-file-list="false"
 | 
			
		||||
                  :http-request="afterRead"
 | 
			
		||||
                  accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf,.mp4,.mp3"
 | 
			
		||||
              >
 | 
			
		||||
                <el-icon class="avatar-uploader-icon">
 | 
			
		||||
                  <Plus/>
 | 
			
		||||
                </el-icon>
 | 
			
		||||
              </el-upload>
 | 
			
		||||
            </div>
 | 
			
		||||
          </el-col>
 | 
			
		||||
          <el-col :span="3" v-for="file in fileList" :key="file.url">
 | 
			
		||||
            <div class="grid-content">
 | 
			
		||||
              <el-tooltip
 | 
			
		||||
                  class="box-item"
 | 
			
		||||
                  effect="dark"
 | 
			
		||||
                  :content="file.name"
 | 
			
		||||
                  placement="top">
 | 
			
		||||
                <el-image :src="file.url" fit="cover" v-if="isImage(file.ext)" @click="insertURL(file)"/>
 | 
			
		||||
                <el-image :src="GetFileIcon(file.ext)" fit="cover" v-else @click="insertURL(file)"/>
 | 
			
		||||
              </el-tooltip>
 | 
			
		||||
 | 
			
		||||
              <div class="opt">
 | 
			
		||||
                <el-button type="danger" size="small" :icon="Delete" @click="removeFile(file)" circle/>
 | 
			
		||||
      <el-scrollbar ref="scrollbarRef" max-height="80vh" style="height: 100%;" @scroll="onScroll">
 | 
			
		||||
        <div class="file-list">
 | 
			
		||||
          <el-row :gutter="20">
 | 
			
		||||
            <el-col :span="3">
 | 
			
		||||
              <div class="grid-content">
 | 
			
		||||
                <el-upload
 | 
			
		||||
                    class="avatar-uploader"
 | 
			
		||||
                    :auto-upload="true"
 | 
			
		||||
                    :show-file-list="false"
 | 
			
		||||
                    :http-request="afterRead"
 | 
			
		||||
                    accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf,.mp4,.mp3"
 | 
			
		||||
                >
 | 
			
		||||
                  <el-icon class="avatar-uploader-icon">
 | 
			
		||||
                    <Plus/>
 | 
			
		||||
                  </el-icon>
 | 
			
		||||
                </el-upload>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </el-col>
 | 
			
		||||
        </el-row>
 | 
			
		||||
      </div>
 | 
			
		||||
            </el-col>
 | 
			
		||||
            <el-col :span="3" v-for="file in fileData.items" :key="file.url">
 | 
			
		||||
              <div class="grid-content">
 | 
			
		||||
                <el-tooltip
 | 
			
		||||
                    class="box-item"
 | 
			
		||||
                    effect="dark"
 | 
			
		||||
                    :content="file.name"
 | 
			
		||||
                    placement="top">
 | 
			
		||||
                  <el-image :src="file.url" fit="cover" v-if="isImage(file.ext)" @click="insertURL(file)"/>
 | 
			
		||||
                  <el-image :src="GetFileIcon(file.ext)" fit="cover" v-else @click="insertURL(file)"/>
 | 
			
		||||
                </el-tooltip>
 | 
			
		||||
 | 
			
		||||
                <div class="opt">
 | 
			
		||||
                  <el-button type="danger" size="small" :icon="Delete" @click="removeFile(file)" circle/>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </el-col>
 | 
			
		||||
          </el-row>
 | 
			
		||||
          <el-row justify="center" v-if="!fileData.isLastPage" @click="fetchFiles(fileData.page)">
 | 
			
		||||
            <el-link>加载更多</el-link>
 | 
			
		||||
            
 | 
			
		||||
          </el-row>
 | 
			
		||||
        </div>        
 | 
			
		||||
      </el-scrollbar>
 | 
			
		||||
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
  </el-container>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {ref} from "vue";
 | 
			
		||||
import {reactive, ref} from "vue";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {Delete, Plus} from "@element-plus/icons-vue";
 | 
			
		||||
@@ -64,15 +71,45 @@ const props = defineProps({
 | 
			
		||||
const emits = defineEmits(['selected']);
 | 
			
		||||
const show = ref(false)
 | 
			
		||||
const fileList = ref([])
 | 
			
		||||
const scrollbarRef = ref(null)
 | 
			
		||||
const fileData = reactive({
 | 
			
		||||
  items:[],
 | 
			
		||||
  page: 1,
 | 
			
		||||
  isLastPage: true,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const fetchFiles = () => {
 | 
			
		||||
  show.value = true
 | 
			
		||||
  httpPost("/api/upload/list").then(res => {
 | 
			
		||||
    fileList.value = res.data.items
 | 
			
		||||
const fetchFiles = (pageNo) => {
 | 
			
		||||
  if(pageNo === 1) show.value = true
 | 
			
		||||
  httpPost("/api/upload/list", { page: pageNo || 1, page_size: 30 }).then(res => {
 | 
			
		||||
    const { items, page, total_page } = res.data
 | 
			
		||||
 | 
			
		||||
    if(page === 1){
 | 
			
		||||
      fileData.items = items
 | 
			
		||||
    }else{
 | 
			
		||||
      fileData.items = [...fileData.items, ...items]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fileData.isLastPage = (page === total_page)
 | 
			
		||||
 | 
			
		||||
    if(!fileData.isLastPage){
 | 
			
		||||
      fileData.page = page + 1
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// el-scrollbar 滚动回调
 | 
			
		||||
const onScroll = (options) => {
 | 
			
		||||
  const wrapRef = scrollbarRef.value.wrapRef
 | 
			
		||||
  scrollbarRef.value.moveY = wrapRef.scrollTop * 100 / wrapRef.clientHeight
 | 
			
		||||
  scrollbarRef.value.moveX = wrapRef.scrollLeft * 100 / wrapRef.clientWidth
 | 
			
		||||
  const poor = wrapRef.scrollHeight - wrapRef.clientHeight
 | 
			
		||||
  // 判断滚动到底部 自动加载数据
 | 
			
		||||
  if (options.scrollTop + 2 >= poor && !fileData.isLastPage) {
 | 
			
		||||
    fetchFiles(fileData.page)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const afterRead = (file) => {
 | 
			
		||||
  const formData = new FormData();
 | 
			
		||||
@@ -120,9 +157,10 @@ const insertURL = (file) => {
 | 
			
		||||
 | 
			
		||||
    .el-dialog__body {
 | 
			
		||||
      //padding 0
 | 
			
		||||
      overflow hidden
 | 
			
		||||
 | 
			
		||||
      .file-list {
 | 
			
		||||
 | 
			
		||||
        margin-right 10px
 | 
			
		||||
        .grid-content {
 | 
			
		||||
          margin-bottom 10px
 | 
			
		||||
          position relative
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="list-box" ref="containerRef">
 | 
			
		||||
  <div class="item__list-box" ref="containerRef">
 | 
			
		||||
    <el-row :gutter="gap">
 | 
			
		||||
      <el-col v-for="item in items" :key="item.id" :span="span" :style="{marginBottom:gap+'px'} ">
 | 
			
		||||
        <slot :item="item"></slot>
 | 
			
		||||
@@ -54,9 +54,10 @@ const calcSpan = () => {
 | 
			
		||||
window.onresize = () => calcSpan()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="stylus">
 | 
			
		||||
<style lang="stylus">
 | 
			
		||||
 | 
			
		||||
.list-box {
 | 
			
		||||
.item__list-box {
 | 
			
		||||
  width 100%
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -170,13 +170,13 @@ const routes = [
 | 
			
		||||
            {
 | 
			
		||||
                path: '/admin/app',
 | 
			
		||||
                name: 'admin-app',
 | 
			
		||||
                meta: {title: '应用管理'},
 | 
			
		||||
                meta: {title: '应用列表'},
 | 
			
		||||
                component: () => import('@/views/admin/Apps.vue'),
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                path: '/admin/app/type',
 | 
			
		||||
                name: 'admin-app-type',
 | 
			
		||||
                meta: {title: '应用管理'},
 | 
			
		||||
                meta: {title: '应用分类'},
 | 
			
		||||
                component: () => import('@/views/admin/AppType.vue'),
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,21 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
 | 
			
		||||
    <div class="page-apps custom-scroll">
 | 
			
		||||
      <div class="inner" :style="{height: listBoxHeight + 'px'}">
 | 
			
		||||
      <div class="apps-type-nav">
 | 
			
		||||
        <el-scrollbar>
 | 
			
		||||
          <ul class="scrollbar-type-nav">
 | 
			
		||||
            <li :class="{active: typeId === ''}" @click="getAppList('')">全部分类</li>
 | 
			
		||||
            <li v-for="item in appTypes" :key="item.id" :class="{active: typeId === item.id}" @click="getAppList(item.id)">
 | 
			
		||||
              <div class="image" v-if="item.icon">
 | 
			
		||||
                <el-image :src="item.icon" fit="cover"/>
 | 
			
		||||
              </div>
 | 
			
		||||
              {{ item.name }}
 | 
			
		||||
            </li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </el-scrollbar>
 | 
			
		||||
      </div>      
 | 
			
		||||
      <div class="app-list-container" :style="{height: listBoxHeight + 'px'}">
 | 
			
		||||
        <ItemList :items="list" v-if="list.length > 0" :gap="15" :width="300">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
            <div class="item">
 | 
			
		||||
@@ -50,6 +64,9 @@
 | 
			
		||||
            <!--            </div>-->
 | 
			
		||||
          </template>
 | 
			
		||||
        </ItemList>
 | 
			
		||||
        <div v-else style="width: 100%">
 | 
			
		||||
          <el-empty description="暂无数据" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -65,23 +82,17 @@ import {useRouter} from "vue-router";
 | 
			
		||||
import {useSharedStore} from "@/store/sharedata";
 | 
			
		||||
import ItemList from "@/components/ItemList.vue";
 | 
			
		||||
 | 
			
		||||
const listBoxHeight = window.innerHeight - 87
 | 
			
		||||
const listBoxHeight = window.innerHeight - 133
 | 
			
		||||
 | 
			
		||||
const typeId = ref('')
 | 
			
		||||
const appTypes = ref([])
 | 
			
		||||
const list = ref([])
 | 
			
		||||
const roles = ref([])
 | 
			
		||||
const store = useSharedStore();
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  httpGet("/api/app/list").then((res) => {
 | 
			
		||||
    const items = res.data
 | 
			
		||||
    // 处理 hello message
 | 
			
		||||
    for (let i = 0; i < items.length; i++) {
 | 
			
		||||
      items[i].intro = substr(items[i].hello_msg, 80)
 | 
			
		||||
    }
 | 
			
		||||
    list.value = items
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("获取应用失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  getAppType()
 | 
			
		||||
  getAppList()
 | 
			
		||||
  getRoles()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@@ -93,6 +104,28 @@ const getRoles = () => {
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getAppType = () => {
 | 
			
		||||
  httpGet("/api/app/type/list").then((res) => {
 | 
			
		||||
    appTypes.value = res.data
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("获取分类失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getAppList = (tid = '') => {
 | 
			
		||||
  typeId.value = tid;
 | 
			
		||||
  httpGet("/api/app/list", { tid }).then((res) => {
 | 
			
		||||
    const items = res.data
 | 
			
		||||
    // 处理 hello message
 | 
			
		||||
    for (let i = 0; i < items.length; i++) {
 | 
			
		||||
      items[i].intro = substr(items[i].hello_msg, 80)
 | 
			
		||||
    }
 | 
			
		||||
    list.value = items
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("获取应用失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const updateRole = (row, opt) => {
 | 
			
		||||
  checkSession().then(() => {
 | 
			
		||||
    const title = ref("")
 | 
			
		||||
 
 | 
			
		||||
@@ -141,8 +141,8 @@
 | 
			
		||||
          :total="total"/>
 | 
			
		||||
      </div>
 | 
			
		||||
    </el-container>
 | 
			
		||||
    <black-dialog v-model:show="showDialog" title="预览视频" hide-footer @cancal="showDialog = false" :width="1000">
 | 
			
		||||
      <video style="width: 100%;" :src="currentVideoUrl"  preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog">
 | 
			
		||||
    <black-dialog v-model:show="showDialog" title="预览视频" hide-footer @cancal="showDialog = false" width="auto">
 | 
			
		||||
      <video style="width: 100%; max-height: 90vh;" :src="currentVideoUrl"  preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog">
 | 
			
		||||
        您的浏览器不支持视频播放
 | 
			
		||||
      </video>
 | 
			
		||||
    </black-dialog>    
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,214 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="container app-type" >
 | 
			
		||||
  <div class="container app-type" v-loading="loading">
 | 
			
		||||
    <div class="handle-box">
 | 
			
		||||
      <el-button type="primary" :icon="Plus" @click="add">新增</el-button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <el-row>
 | 
			
		||||
      <el-table :data="items" :row-key="row => row.id" table-layout="auto">
 | 
			
		||||
        <el-table-column type="selection" width="38"></el-table-column>
 | 
			
		||||
        <el-table-column prop="name" label="分类名称">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
            <span class="sort" :data-id="scope.row.id">
 | 
			
		||||
              <i class="iconfont icon-drag"></i>
 | 
			
		||||
              {{ scope.row.name }}
 | 
			
		||||
            </span>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
        <el-table-column label="图标" prop="icon">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
            <el-image v-if="scope.row.icon" :src="scope.row.icon" style="width: 45px; height: 45px; border-radius: 50%"/>
 | 
			
		||||
            <el-tag type="info" v-else>无图标</el-tag>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
        <el-table-column prop="enabled" label="启用状态">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
            <el-switch v-model="scope.row['enabled']" @change="enableSet(scope.row)"/>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
        <el-table-column label="操作" width="180">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
            <el-button size="small" type="primary" @click="edit(scope.row)">编辑</el-button>
 | 
			
		||||
            <el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row)" :width="200">
 | 
			
		||||
              <template #reference>
 | 
			
		||||
                <el-button size="small" type="danger">删除</el-button>
 | 
			
		||||
              </template>
 | 
			
		||||
            </el-popconfirm>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
      </el-table>
 | 
			
		||||
    </el-row>
 | 
			
		||||
 | 
			
		||||
    <el-dialog
 | 
			
		||||
        v-model="showDialog"
 | 
			
		||||
        :title="title"
 | 
			
		||||
        :close-on-click-modal="false"
 | 
			
		||||
        style="width: 90%; max-width: 600px;"
 | 
			
		||||
    >
 | 
			
		||||
      <el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
 | 
			
		||||
        <el-form-item label="分类名称:" prop="name">
 | 
			
		||||
          <el-input v-model="item.name" autocomplete="off"/>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="应用图标:" prop="icon">
 | 
			
		||||
          <el-input v-model="item.icon">
 | 
			
		||||
            <template #append>
 | 
			
		||||
              <el-upload
 | 
			
		||||
                  :auto-upload="true"
 | 
			
		||||
                  :show-file-list="false"
 | 
			
		||||
                  :http-request="uploadImg"
 | 
			
		||||
              >
 | 
			
		||||
                上传
 | 
			
		||||
              </el-upload>
 | 
			
		||||
            </template>
 | 
			
		||||
          </el-input>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="启用状态:" prop="enable">
 | 
			
		||||
          <el-switch v-model="item.enabled" />
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
      </el-form>
 | 
			
		||||
 | 
			
		||||
      <template #footer>
 | 
			
		||||
        <span class="dialog-footer">
 | 
			
		||||
          <el-button @click="showDialog = false">取消</el-button>
 | 
			
		||||
          <el-button type="primary" @click="save">提交</el-button>
 | 
			
		||||
        </span>
 | 
			
		||||
      </template>
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {onMounted, onUnmounted, reactive, ref} from "vue";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {removeArrayItem} from "@/utils/libs";
 | 
			
		||||
import {Sortable} from "sortablejs";
 | 
			
		||||
import Compressor from "compressorjs";
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
// 变量定义
 | 
			
		||||
const items = ref([])
 | 
			
		||||
const item = ref({})
 | 
			
		||||
const showDialog = ref(false)
 | 
			
		||||
const title = ref("")
 | 
			
		||||
const rules = reactive({
 | 
			
		||||
  name: [{required: true, message: '请输入分类名称', trigger: 'change',}],
 | 
			
		||||
})
 | 
			
		||||
const loading = ref(true)
 | 
			
		||||
const formRef = ref(null)
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
// 获取数据
 | 
			
		||||
const fetchData = () => {
 | 
			
		||||
  httpGet('/api/admin/app/type/list').then((res) => {
 | 
			
		||||
    if (res.data) {
 | 
			
		||||
      items.value = res.data
 | 
			
		||||
    }
 | 
			
		||||
    loading.value = false
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    ElMessage.error("获取数据失败");
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  fetchData()
 | 
			
		||||
  const drawBodyWrapper = document.querySelector('.el-table__body tbody')
 | 
			
		||||
 | 
			
		||||
  // 初始化拖动排序插件
 | 
			
		||||
  Sortable.create(drawBodyWrapper, {
 | 
			
		||||
    sort: true,
 | 
			
		||||
    animation: 500,
 | 
			
		||||
    onEnd({newIndex, oldIndex, from}) {
 | 
			
		||||
      if (oldIndex === newIndex) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const sortedData = Array.from(from.children).map(row => row.querySelector('.sort').getAttribute('data-id'));
 | 
			
		||||
      const ids = []
 | 
			
		||||
      const sorts = []
 | 
			
		||||
      sortedData.forEach((id, index) => {
 | 
			
		||||
        ids.push(parseInt(id))
 | 
			
		||||
        sorts.push(index + 1)
 | 
			
		||||
        items.value[index].sort_num = index + 1
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      httpPost("/api/admin/app/type/sort", {ids: ids, sorts: sorts}).then(() => {
 | 
			
		||||
      }).catch(e => {
 | 
			
		||||
        ElMessage.error("排序失败:" + e.message)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const add = function () {
 | 
			
		||||
  title.value = "新增分类"
 | 
			
		||||
  showDialog.value = true
 | 
			
		||||
  item.value = { enabled: true, }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const edit = function (row) {
 | 
			
		||||
  title.value = "修改分类"
 | 
			
		||||
  showDialog.value = true
 | 
			
		||||
  item.value = row
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const save = function () {
 | 
			
		||||
  formRef.value.validate((valid) => {
 | 
			
		||||
    if (!item.value.sort_num) {
 | 
			
		||||
      item.value.sort_num = items.value.length
 | 
			
		||||
    }
 | 
			
		||||
    if (valid) {
 | 
			
		||||
      showDialog.value = false
 | 
			
		||||
      httpPost('/api/admin/app/type/save', item.value).then(() => {
 | 
			
		||||
        ElMessage.success('操作成功!')
 | 
			
		||||
        fetchData()
 | 
			
		||||
      }).catch((e) => {
 | 
			
		||||
        ElMessage.error('操作失败,' + e.message)
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 设置启用状态
 | 
			
		||||
const enableSet = (row) => {
 | 
			
		||||
  httpPost('/api/admin/app/type/enable', {id: row.id, enabled: row.enabled}).then(() => {
 | 
			
		||||
    ElMessage.success("操作成功!")
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("操作失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 删除数据
 | 
			
		||||
const remove = function (row) {
 | 
			
		||||
  httpGet('/api/admin/app/type/remove?id=' + row.id).then(() => {
 | 
			
		||||
    ElMessage.success("删除成功!")
 | 
			
		||||
    items.value = removeArrayItem(items.value, row, (v1, v2) => {
 | 
			
		||||
      return v1.id === v2.id
 | 
			
		||||
    })
 | 
			
		||||
  }).catch((e) => {
 | 
			
		||||
    ElMessage.error("删除失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 图片上传
 | 
			
		||||
const uploadImg = (file) => {
 | 
			
		||||
  // 压缩图片并上传
 | 
			
		||||
  new Compressor(file.file, {
 | 
			
		||||
    quality: 0.6,
 | 
			
		||||
    success(result) {
 | 
			
		||||
      const formData = new FormData();
 | 
			
		||||
      formData.append('file', result, result.name);
 | 
			
		||||
      // 执行上传操作
 | 
			
		||||
      httpPost('/api/admin/upload', formData).then((res) => {
 | 
			
		||||
        item.value.icon = res.data.url
 | 
			
		||||
        ElMessage.success('上传成功')
 | 
			
		||||
      }).catch((e) => {
 | 
			
		||||
        ElMessage.error('上传失败:' + e.message)
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    error(e) {
 | 
			
		||||
      ElMessage.error('上传失败:' + e.message)
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -23,6 +23,7 @@
 | 
			
		||||
            </span>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
        <el-table-column label="应用类型" prop="type_name"/>
 | 
			
		||||
        <el-table-column label="应用标识" prop="key"/>
 | 
			
		||||
        <el-table-column label="绑定模型" prop="model_name"/>
 | 
			
		||||
        <el-table-column label="启用状态">
 | 
			
		||||
@@ -62,6 +63,21 @@
 | 
			
		||||
              autocomplete="off"
 | 
			
		||||
          />
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="应用分类:" prop="tid">
 | 
			
		||||
          <el-select
 | 
			
		||||
              v-model="role.tid"
 | 
			
		||||
              filterable
 | 
			
		||||
              placeholder="请选择分类"
 | 
			
		||||
              clearable
 | 
			
		||||
          >
 | 
			
		||||
            <el-option
 | 
			
		||||
                v-for="item in appTypes"
 | 
			
		||||
                :key="item.id"
 | 
			
		||||
                :label="item.name"
 | 
			
		||||
                :value="item.id"
 | 
			
		||||
            />
 | 
			
		||||
          </el-select>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
 | 
			
		||||
        <el-form-item label="应用标志:" prop="key">
 | 
			
		||||
          <el-input
 | 
			
		||||
@@ -195,6 +211,7 @@ const rules = reactive({
 | 
			
		||||
  hello_msg: [{required: true, message: '请输入打招呼信息', trigger: 'change',}]
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const appTypes = ref([])
 | 
			
		||||
const models = ref([])
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  fetchData()
 | 
			
		||||
@@ -206,11 +223,25 @@ onMounted(() => {
 | 
			
		||||
    ElMessage.error("获取AI模型数据失败");
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // get app type
 | 
			
		||||
  httpGet('/api/admin/app/type/list?enable=1').then((res) => {
 | 
			
		||||
    appTypes.value = res.data
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    ElMessage.error("获取应用分类数据失败");
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const fetchData = () => {
 | 
			
		||||
  // 获取应用列表
 | 
			
		||||
  httpGet('/api/admin/role/list').then((res) => {
 | 
			
		||||
    // 初始化数据
 | 
			
		||||
    // const arr = res.data;
 | 
			
		||||
    // for (let i = 0; i < arr.length; i++) {
 | 
			
		||||
    //   if(arr[i].model_id == 0){
 | 
			
		||||
    //     arr[i].model_id = ''
 | 
			
		||||
    //   }
 | 
			
		||||
    // }
 | 
			
		||||
    tableData.value = res.data
 | 
			
		||||
    sortedTableData.value = copyObj(tableData.value)
 | 
			
		||||
    loading.value = false
 | 
			
		||||
 
 | 
			
		||||
@@ -21,5 +21,11 @@ module.exports = defineConfig({
 | 
			
		||||
    devServer: {
 | 
			
		||||
        allowedHosts: "all",
 | 
			
		||||
        port: 8888,
 | 
			
		||||
        proxy: {
 | 
			
		||||
            '/static/upload/': {
 | 
			
		||||
              target:  process.env.VUE_APP_API_HOST,
 | 
			
		||||
              changeOrigin: true,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
    }
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user