mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	feat: 应用分类功能
This commit is contained in:
		@@ -2,10 +2,49 @@
 | 
			
		||||
  background-color: #282c34;
 | 
			
		||||
  height 100%
 | 
			
		||||
 | 
			
		||||
  .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
 | 
			
		||||
      }
 | 
			
		||||
      &.active{
 | 
			
		||||
        background #21aa93;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  .inner {
 | 
			
		||||
    display flex
 | 
			
		||||
    color #ffffff
 | 
			
		||||
    padding 15px;
 | 
			
		||||
    padding 2px 15px;
 | 
			
		||||
    overflow-y visible
 | 
			
		||||
    overflow-x hidden
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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,6 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
 | 
			
		||||
    <div class="page-apps custom-scroll">
 | 
			
		||||
      <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">
 | 
			
		||||
                <el-image :src="item.icon" fit="cover"/>
 | 
			
		||||
              </div>
 | 
			
		||||
              {{ item.name }}
 | 
			
		||||
            </li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </el-scrollbar>
 | 
			
		||||
      </div>      
 | 
			
		||||
      <div class="inner" :style="{height: listBoxHeight + 'px'}">
 | 
			
		||||
        <ItemList :items="list" v-if="list.length > 0" :gap="15" :width="300">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
@@ -65,23 +79,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 +101,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,213 @@
 | 
			
		||||
<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 :src="scope.row.icon" style="width: 45px; height: 45px; border-radius: 50%"/>
 | 
			
		||||
          </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.obj_key.replace('./static', '/static')
 | 
			
		||||
        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