Browse Source

Merge branch 'master' of http://git.izouma.com/xiongzhu/chat-admin

panhui 2 năm trước cách đây
mục cha
commit
ecb6f82c0d

+ 1 - 0
package.json

@@ -19,6 +19,7 @@
     "element-plus": "^2.3.3",
     "pinia": "^2.0.32",
     "qs": "^6.11.1",
+    "resolve-url": "^0.2.1",
     "vue": "^3.2.47",
     "vue-router": "^4.1.6"
   },

+ 112 - 0
src/components/MultiUpload.vue

@@ -0,0 +1,112 @@
+<template>
+    <section>
+        <el-upload
+            list-type="picture-card"
+            accept="image/*"
+            :action="uploadUrl"
+            :on-preview="handlePictureCardPreview"
+            :on-success="handleSuccess"
+            :on-remove="handleRemove"
+            v-model:file-list="fileList"
+            @before-upload="beforeUpload"
+            multiple
+        >
+            <el-icon>
+                <Plus />
+            </el-icon>
+            <template #tip>
+                <div class="el-upload__tip">
+                    <slot></slot>
+                </div>
+            </template>
+        </el-upload>
+        <ElImageViewer v-if="showPreview" :url-list="[previewUrl]" teleported @close="showPreview = false" />
+    </section>
+</template>
+<script setup>
+import resolveUrl from 'resolve-url'
+import { ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Plus } from '@vicons/tabler'
+import { useImageSize } from '@/utils/imageSize'
+
+const props = defineProps({
+    modelValue: Array,
+    maxWidth: {
+        type: Number,
+        default: -1
+    },
+    maxHeight: {
+        type: Number,
+        default: -1
+    },
+    maxSize: {
+        type: Number,
+        default: 1024 * 1024
+    }
+})
+const emit = defineEmits(['update:modelValue'])
+const uploadUrl = resolveUrl(import.meta.env.VITE_API_BASE_URL, '/api/file/upload')
+const fileList = ref([])
+const loading = ref(false)
+const showPreview = ref(false)
+const previewUrl = ref(null)
+watch(
+    () => props.modelValue,
+    (val) => {
+        if (val && val.join() === fileList.value.map((i) => i.realUrl).join()) {
+            return
+        }
+        updateFileList(val)
+    }
+)
+updateFileList(props.modelValue || [])
+function updateFileList(list) {
+    fileList.value = list.map((i) => {
+        return {
+            url: i,
+            realUrl: i
+        }
+    })
+}
+
+async function beforeUpload(file) {
+    if (!/^image/.test(file.type)) {
+        this.$message.error('只能上传图片格式!')
+        return false
+    }
+    if (this.maxSize > 0 && file.size > this.maxSize) {
+        this.$message.error('上传图片大小不能超过 ' + this.maxSize / 1024 + 'KB!')
+        return false
+    }
+    const { width, height } = await useImageSize(URL.createObjectURL(file))
+    if (props.maxWidth > 0 && width > props.maxWidth) {
+        this.$message.error('上传图片宽度不能超过 ' + props.maxWidth + 'px!')
+        return false
+    } else if (props.maxHeight > 0 && height > props.maxHeight) {
+        this.$message.error('上传图片高度不能超过 ' + props.maxHeight + 'px!')
+        return false
+    } else {
+        loading.value = true
+        return true
+    }
+}
+function handlePictureCardPreview(file) {
+    previewUrl.value = file.url || file.realUrl
+    showPreview.value = true
+}
+function handleSuccess(res, file, fileList) {
+    file.realUrl = res.url
+    emit(
+        'update:modelValue',
+        fileList.map((i) => i.realUrl)
+    )
+}
+function handleRemove(file, fileList) {
+    emit(
+        'update:modelValue',
+        fileList.map((i) => i.realUrl)
+    )
+}
+</script>
+<style lang="less" scoped></style>

+ 0 - 8
src/components/PagingTable.vue

@@ -36,11 +36,6 @@ const props = defineProps({
     order: {
         type: Object,
         default: () => ({ createdAt: 'DESC' })
-    },
-    //冻结
-    freeze: {
-        type: Boolean,
-        default: false
     }
 })
 const search = computed(() => {
@@ -73,9 +68,6 @@ const pageConfig = useStorage('pageConfig', {
 const total = ref(0)
 const loading = ref(false)
 async function getData() {
-    if (props.freeze) {
-        return
-    }
     try {
         loading.value = true
         const res = await http.post(props.url, {

+ 173 - 0
src/components/SingleUpload.vue

@@ -0,0 +1,173 @@
+<template>
+    <el-upload
+        v-model:file-list="fileList"
+        class="single-upload"
+        :action="uploadUrl"
+        :show-file-list="false"
+        :on-success="onSuccess"
+        :before-upload="beforeUpload"
+        accept="image/*"
+        @on-change="onChange"
+    >
+        <template #trigger>
+            <div
+                class="w-40 h-40 bg-neutral-100 dark:bg-neutral-900 rounded-md cursor-pointer border border-dashed border-gray-500 hover:border-blue-500 flex justify-center items-center overflow-hidden relative"
+                v-loading="loading"
+            >
+                <img v-if="imageUrl" :src="imageUrl" class="w-full h-full object-cover" />
+                <Plus v-if="!imageUrl && !loading" class="w-10 h-10 text-gray-400"> </Plus>
+                <div v-if="imageUrl" class="absolute bottom-4 left-0 right-0 flex items-center justify-center">
+                    <Eye
+                        class="w-5 h-5 text-neutral-100 drop-shadow-md cursor-pointer"
+                        @click.stop="showPreview = true"
+                    />
+                    <Trash class="w-5 h-5 text-neutral-100 drop-shadow-md cursor-pointer ml-4" @click.stop="clear" />
+                </div>
+            </div>
+        </template>
+        <template #tip>
+            <div class="el-upload__tip">
+                <slot></slot>
+            </div>
+        </template>
+    </el-upload>
+    <ElImageViewer v-if="showPreview" :url-list="[imageUrl]" teleported @close="showPreview = false" />
+</template>
+<script setup>
+import { ref, unref, watch } from 'vue'
+import resolveUrl from 'resolve-url'
+import { Plus, Upload, Trash, Eye } from '@vicons/tabler'
+import { useImageSize } from '@/utils/imageSize'
+const props = defineProps({
+    modelValue: String,
+    usePrefix: {
+        type: Boolean,
+        default: true
+    },
+    url: {
+        type: String
+    },
+    maxWidth: {
+        type: Number,
+        default: -1
+    },
+    maxHeight: {
+        type: Number,
+        default: -1
+    },
+    maxSize: {
+        type: Number,
+        default: 1024 * 1024
+    }
+})
+const emit = defineEmits(['update:modelValue'])
+const uploadUrl = resolveUrl(import.meta.env.VITE_API_BASE_URL, '/api/file/upload')
+const loading = ref(false)
+const fileList = ref([])
+const imageUrl = ref(null)
+const showPreview = ref(false)
+
+if (props.modelValue) {
+    imageUrl.value = unref(props.modelValue)
+    fileList.value = [
+        {
+            url: unref(props.modelValue)
+        }
+    ]
+}
+watch(
+    () => props.modelValue,
+    (val) => {
+        imageUrl.value = val
+        fileList.value = [
+            {
+                url: val
+            }
+        ]
+    }
+)
+function onSuccess(res, file) {
+    loading.value = false
+    imageUrl.value = URL.createObjectURL(file.raw)
+    emit('update:modelValue', res.url)
+}
+function onError(err, file, fileList) {
+    loading.value = false
+}
+async function beforeUpload(file) {
+    if (!/^image/.test(file.type)) {
+        this.$message.error('只能上传图片格式!')
+        return false
+    }
+    if (this.maxSize > 0 && file.size > this.maxSize) {
+        this.$message.error('上传图片大小不能超过 ' + this.maxSize / 1024 + 'KB!')
+        return false
+    }
+    const { width, height } = await useImageSize(URL.createObjectURL(file))
+    if (props.maxWidth > 0 && width > props.maxWidth) {
+        this.$message.error('上传图片宽度不能超过 ' + props.maxWidth + 'px!')
+        return false
+    } else if (props.maxHeight > 0 && height > props.maxHeight) {
+        this.$message.error('上传图片高度不能超过 ' + props.maxHeight + 'px!')
+        return false
+    } else {
+        loading.value = true
+        return true
+    }
+}
+
+function onChange(e) {
+    console.log(e)
+}
+function clear() {
+    emit('update:modelValue', null)
+}
+</script>
+<style lang="less" scoped>
+.single-uploader-icon {
+    color: var(--el-text-color-secondary);
+    border: 1px dashed var(--el-border-color);
+    border-radius: var(--el-border-radius-base);
+    background-color: var(--el-bg-color);
+}
+
+.upload-image {
+    height: 178px;
+    display: block;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+
+    &:hover {
+        border-color: #409eff;
+    }
+}
+
+.wrapper {
+    position: relative;
+    font-size: 12px;
+}
+
+.single-upload .el-upload {
+    position: relative;
+}
+
+.loading {
+    position: absolute;
+    top: 1px;
+    bottom: 1px;
+    left: 1px;
+    right: 1px;
+    margin: auto;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: rgba(255, 255, 255, 0.6);
+    color: #333;
+    font-size: 24px;
+    border-radius: 7px;
+    overflow: hidden;
+}
+</style>

+ 9 - 1
src/enums/index.js

@@ -23,5 +23,13 @@ export const MemberType = {
 export const UserRole = {
     user: '普通用户',
     admin: '管理员',
-    api: 'API用户'
+    api: 'API用户',
+    org: '企业用户'
+}
+
+export const FileStatus = {
+    pending: '待处理',
+    processing: '处理中',
+    done: '完成',
+    failed: '失败'
 }

+ 32 - 0
src/router/index.js

@@ -141,6 +141,38 @@ const router = createRouter({
                     meta: {
                         title: '系统管理'
                     }
+                },
+                {
+                    path: 'org',
+                    name: 'org',
+                    component: () => import('../views/OrgView.vue'),
+                    meta: {
+                        title: '企业管理'
+                    }
+                },
+                {
+                    path: 'orgEdit',
+                    name: 'orgEdit',
+                    component: () => import('../views/OrgEditView.vue'),
+                    meta: {
+                        title: '企业信息'
+                    }
+                },
+                {
+                    path: 'knowledge',
+                    name: 'knowledge',
+                    component: () => import('../views/KnowledgeView.vue'),
+                    meta: {
+                        title: '知识库管理'
+                    }
+                },
+                {
+                    path: 'orgUser',
+                    name: 'orgUser',
+                    component: () => import('../views/OrgUserView.vue'),
+                    meta: {
+                        title: '席位管理'
+                    }
                 }
             ]
         }

+ 13 - 0
src/utils/imageSize.js

@@ -0,0 +1,13 @@
+export function useImageSize(src) {
+    return new Promise((resolve, reject) => {
+        const img = new Image()
+        img.onload = () => {
+            resolve({
+                width: img.width,
+                height: img.height
+            })
+        }
+        img.onerror = reject
+        img.src = src
+    })
+}

+ 4 - 8
src/views/ApiUserEditView.vue

@@ -77,19 +77,15 @@ import { http } from '@/plugins/http'
 import { ElMessage } from 'element-plus'
 import { useClipboard } from '@vueuse/core'
 import { Plus, UploadFilled } from '@element-plus/icons-vue'
+import { useUserStore } from '@/stores/user'
+import { storeToRefs } from 'pinia'
 
 const fileList = []
 const where = ref({})
 const timeFormatter = useTimeFormatter()
 const model = ref({})
-http.get(`/auth/admin/getRole`).then((res) => {
-    if (res === 'api') {
-        http.get('/admin/users/get').then((res) => {
-            where.value = { userId: res.id }
-            getData()
-        })
-    }
-})
+const { user } = storeToRefs(useUserStore())
+where.value = { userId: user.value.id }
 function getData() {
     http.post('/apiUser', {
         page: {

+ 5 - 7
src/views/ApiUserView.vue

@@ -83,6 +83,8 @@ import { UserRole } from '@/enums'
 import { http } from '@/plugins/http'
 import { ElMessage } from 'element-plus'
 import { useClipboard } from '@vueuse/core'
+import { useUserStore } from '@/stores/user'
+import { storeToRefs } from 'pinia'
 
 const fileList = []
 const where = ref({})
@@ -91,13 +93,9 @@ const table = ref(null)
 const model = ref({})
 const name = ''
 const filename = ''
-http.get(`/auth/admin/getRole`).then((res) => {
-    if (res === 'api') {
-        http.get('/admin/users/get').then((res) => {
-            where.value = { userId: res.id }
-        })
-    }
-})
+const { user } = storeToRefs(useUserStore())
+where.value = { userId: user.value.id }
+
 const { showEditDialog, onEdit } = setupEditDialog(model)
 function uploadFile(file) {
     if (!model.value.code) {

+ 3 - 9
src/views/HomeView.vue

@@ -80,6 +80,7 @@ import { differenceInCalendarDays } from 'date-fns'
 import { onMounted, watch, ref, nextTick, computed } from 'vue'
 import { useDark, useToggle, useResizeObserver } from '@vueuse/core'
 import { http } from '@/plugins/http'
+import { storeToRefs } from 'pinia'
 
 const isDark = useDark({
     storageKey: 'dark-mode-admin'
@@ -87,7 +88,7 @@ const isDark = useDark({
 
 import * as echarts from 'echarts'
 
-const { user } = useUserStore()
+const { user } = storeToRefs(useUserStore())
 
 const time = computed(() => {
     return differenceInCalendarDays(new Date(), new Date(user.createdAt))
@@ -266,15 +267,8 @@ const userDatas = ref({
 const chatDatas = ref({
     today: 0
 })
-const role = ref('admin')
 onMounted(() => {
-    http.get(`/auth/admin/getRole`).then((res) => {
-        if (res === 'api') {
-            role.value = 'api'
-        }
-
-        initChart()
-    })
+    initChart()
 })
 </script>
 

+ 165 - 0
src/views/KnowledgeView.vue

@@ -0,0 +1,165 @@
+<template>
+    <PagingTable url="/knowledge/base" :where="where" ref="table">
+        <template #filter>
+            <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
+            <ElButton :icon="Refresh" @click="table.refresh()"></ElButton>
+        </template>
+        <ElTableColumn prop="id" label="#" width="80" />
+        <ElTableColumn prop="name" label="名称" />
+        <ElTableColumn prop="description" label="描述" />
+        <ElTableColumn prop="orgId" label="企业ID" />
+        <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
+        <ElTableColumn label="操作" align="center" width="200">
+            <template #default="{ row }">
+                <ElButton @click="onEdit(row)">编辑</ElButton>
+                <ElButton @click="showFiles(row)">文件</ElButton>
+                <ElButton @click="deleteRow(row)" type="danger">删除</ElButton>
+            </template>
+        </ElTableColumn>
+    </PagingTable>
+    <EditDialog v-model="showEditDialog" :model="model" :rules="rules" :on-submit="submit" @success="table.refresh()">
+        <ElFormItem prop="name" label="名称">
+            <ElInput v-model="model.name" placeholder="请输入名称" />
+        </ElFormItem>
+        <ElFormItem prop="description" label="描述">
+            <ElInput v-model="model.description" placeholder="请输入描述" />
+        </ElFormItem>
+        <ElFormItem prop="orgId" label="企业ID" v-if="role === 'admin'">
+            <ElInputNumber :controls="false" v-model="model.orgId" placeholder="请输入企业ID" />
+        </ElFormItem>
+    </EditDialog>
+
+    <ElDialog v-model="showFileDialog" :title="selectedBase?.name" width="80%">
+        <PagingTable url="/knowledge/file" :where="{ knowledgeId: selectedBase?.id }" ref="fileTable">
+            <template #filter>
+                <ElButton :icon="Plus" @click="uploadFile()">添加</ElButton>
+                <ElButton :icon="Refresh" @click="fileTable.refresh()"></ElButton>
+            </template>
+            <ElTableColumn prop="id" label="#" width="80" />
+            <ElTableColumn prop="orgId" label="企业ID" />
+            <ElTableColumn prop="knowledgeId" label="知识库ID" />
+            <ElTableColumn prop="fileName" label="文件名" show-overflow-tooltip />
+            <ElTableColumn prop="fileHash" label="文件Hash" show-overflow-tooltip />
+            <ElTableColumn prop="status" label="状态" align="center">
+                <template #default="{ row }">
+                    <ElTag :type="fileStatusTagType(row.status)">{{
+                        fileStatusFormatter(null, null, row.status)
+                    }}</ElTag>
+                </template>
+            </ElTableColumn>
+            <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
+            <ElTableColumn label="操作" align="center" width="100">
+                <template #default="{ row }">
+                    <ElButton @click="deleteFile(row)" type="danger">删除</ElButton>
+                </template>
+            </ElTableColumn>
+        </PagingTable>
+    </ElDialog>
+</template>
+<script setup>
+import { ref, computed, nextTick } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useEnumFormatter, useTimeFormatter } from '@/utils/formatter'
+import { Plus, Refresh } from '@vicons/tabler'
+import EditDialog from '@/components/EditDialog.vue'
+import { setupEditDialog } from '@/utils/editDialog'
+import { http } from '@/plugins/http'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useClipboard } from '@vueuse/core'
+import { storeToRefs } from 'pinia'
+import { useUserStore } from '@/stores/user'
+import { FileStatus } from '@/enums'
+import { useFileDialog } from '@vueuse/core'
+const { user } = storeToRefs(useUserStore())
+const role = computed(() => user.value?.roles[0])
+const where = computed(() => {
+    if (role.value === 'admin') return {}
+    return {
+        orgId: user.value.orgId
+    }
+})
+const timeFormatter = useTimeFormatter()
+const table = ref(null)
+const model = ref({})
+const rules = {
+    name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
+    description: [{ required: true, message: '请输入描述', trigger: 'blur' }],
+    orgId: [{ required: true, message: '请输入企业ID', trigger: 'blur' }]
+}
+const { showEditDialog, onEdit } = setupEditDialog(model)
+async function submit() {
+    if (!model.value.orgId) {
+        model.value.orgId = user.value.orgId
+    }
+    await http.put(model.value.id ? `/knowledge/base/${model.value.id}` : '/knowledge/base', model.value)
+    ElMessage.success('保存成功')
+}
+async function deleteRow(row) {
+    try {
+        await ElMessageBox.confirm('此操作将永久删除数据, 是否继续?', '提示', {
+            type: 'warning'
+        })
+        await http.delete(`/knowledge/base/${row.id}`)
+        ElMessage.success('删除成功')
+        table.value.refresh()
+    } catch (error) {
+        if ('cancel' !== error) ElMessage.error(error.message)
+    }
+}
+const selectedBase = ref(null)
+const fileTable = ref(null)
+function showFiles(row) {
+    selectedBase.value = row
+    showFileDialog.value = true
+    nextTick(() => {
+        fileTable.value.refresh()
+    })
+}
+const showFileDialog = ref(false)
+const fileStatusFormatter = useEnumFormatter(FileStatus)
+function fileStatusTagType(status) {
+    switch (status) {
+        case 'pending':
+            return 'info'
+        case 'processing':
+            return ''
+        case 'done':
+            return 'success'
+        case 'failed':
+            return 'danger'
+        default:
+            return 'info'
+    }
+}
+const uploading = ref(false)
+function uploadFile() {
+    const { files, open, reset, onChange } = useFileDialog({ accept: 'application/pdf, .xlsx' })
+    onChange(async (files) => {
+        console.log(files[0])
+        uploading.value = true
+        try {
+            const formData = new FormData()
+            formData.append('file', files[0])
+            await http.put(`/knowledge/base/${selectedBase.value.id}/file`, formData)
+        } catch (error) {
+            ElMessage.error(error.message)
+        } finally {
+            uploading.value = false
+            fileTable.value.refresh()
+        }
+    })
+    open()
+}
+async function deleteFile(row) {
+    try {
+        await ElMessageBox.confirm('此操作将永久删除数据, 是否继续?', '提示', {
+            type: 'warning'
+        })
+        await http.delete(`/knowledge/file/${row.id}`)
+        ElMessage.success('删除成功')
+        fileTable.value.refresh()
+    } catch (error) {
+        if ('cancel' !== error) ElMessage.error(error.message)
+    }
+}
+</script>

+ 120 - 115
src/views/MainView.vue

@@ -53,162 +53,167 @@ import { ElAside, ElContainer, ElHeader, ElMain, ElMessageBox } from 'element-pl
 import DarkSwitch from '@/components/DarkSwitch.vue'
 import SideMenu from '@/components/SideMenu.vue'
 import { useRoute } from 'vue-router'
-import { ref, watch, shallowRef, inject } from 'vue'
-import LogoSvg from '@/components/LogoSvg.vue'
-import { User, MoodSmile, Wallet,Notebook, Home, Files, Menu2, Settings, Users, License,Database } from '@vicons/tabler'
+import { ref, watch, shallowRef, inject, computed } from 'vue'
+import {
+    User,
+    MoodSmile,
+    Wallet,
+    Notebook,
+    Home,
+    Files,
+    Menu2,
+    Settings,
+    Users,
+    License,
+    Database,
+    Building
+} from '@vicons/tabler'
 import UserAvatar from '@/components/UserAvatar.vue'
 import ChangePwd from '@/components/ChangePwd.vue'
 import { http } from '@/plugins/http'
+import { useUserStore } from '@/stores/user'
+import { storeToRefs } from 'pinia'
 
+const { user } = storeToRefs(useUserStore())
 const route = useRoute()
 const activeMenu = ref(route.name || 'home')
 const isMobile = inject('isMobile')
 const showDrawer = ref(false)
-const menus = ref([
-    {
-        name: 'home',
-        title: '主页',
-        icon: shallowRef(Home)
-    },
-    {
-        name: 'user-parent',
-        title: '用户管理',
-        icon: shallowRef(User),
-        children: [
+const menus = computed(() => {
+    if (!(user.value && user.value.roles)) return []
+    if (user.value.roles[0] === 'admin') {
+        return [
             {
-                name: 'user',
-                title: '用户列表'
+                name: 'home',
+                title: '主页',
+                icon: Home
             },
             {
-                name: 'apiUser',
-                title: 'api用户'
-            }
-        ]
-    },
-    {
-        name: 'membership-parent',
-        title: '会员管理',
-        icon: shallowRef(MoodSmile),
-        children: [
-            {
-                name: 'member',
-                title: '会员列表'
+                name: 'user-parent',
+                title: '用户管理',
+                icon: User,
+                children: [
+                    {
+                        name: 'user',
+                        title: '用户列表'
+                    },
+                    {
+                        name: 'apiUser',
+                        title: 'api用户'
+                    }
+                ]
             },
             {
-                name: 'memberOrder',
-                title: '会员订单'
+                name: 'org-parent',
+                title: '企业管理',
+                icon: Building,
+                children: [
+                    {
+                        name: 'org',
+                        title: '企业管理'
+                    },
+                    {
+                        name: 'knowledge',
+                        title: '知识库管理'
+                    }
+                ]
             },
             {
-                name: 'memberPlan',
-                title: '会员计划'
-            }
-        ]
-    },
-    {
-        name: 'money',
-        title: '提现申请',
-        icon: shallowRef(Wallet),
-        children: [
+                name: 'membership-parent',
+                title: '会员管理',
+                icon: MoodSmile,
+                children: [
+                    {
+                        name: 'member',
+                        title: '会员列表'
+                    },
+                    {
+                        name: 'memberOrder',
+                        title: '会员订单'
+                    },
+                    {
+                        name: 'memberPlan',
+                        title: '会员计划'
+                    }
+                ]
+            },
             {
-                name: 'withdraw',
-                title: '提现申请'
-            }
-        ]
-    },
-    {
-        name: 'mask',
-        title: '面具设置',
-        icon: shallowRef(Settings),
-        children: [
+                name: 'money',
+                title: '提现申请',
+                icon: Wallet,
+                children: [
+                    {
+                        name: 'withdraw',
+                        title: '提现申请'
+                    }
+                ]
+            },
             {
                 name: 'mask',
-                title: '面具设置'
-            }
-        ]
-    },
-    {
-        name: 'chatRole',
-        title: '角色设置',
-        icon: shallowRef(Settings),
-        children: [
+                title: '面具设置',
+                icon: Settings,
+                children: [
+                    {
+                        name: 'mask',
+                        title: '面具设置'
+                    }
+                ]
+            },
             {
                 name: 'chatRole',
-                title: '角色设置'
-            }
-        ]
-    },
-    {
-        name: 'settings',
-        title: '系统设置',
-        icon: shallowRef(Settings),
-        children: [
+                title: '角色设置',
+                icon: Settings,
+                children: [
+                    {
+                        name: 'chatRole',
+                        title: '角色设置'
+                    }
+                ]
+            },
             {
-                name: 'sysConfig',
-                title: '参数设置'
+                name: 'settings',
+                title: '系统设置',
+                icon: Settings,
+                children: [
+                    {
+                        name: 'sysConfig',
+                        title: '参数设置'
+                    }
+                ]
             }
         ]
-    }
-])
-http.get(`/auth/admin/getRole`).then((res) => {
-    if (res === 'api') {
-        menus.value = [
+    } else if (user.value.roles[0] === 'org') {
+        return [
             {
                 name: 'home',
                 title: '主页',
-                icon: shallowRef(Home)
+                icon: Home
             },
-            // {
-            //     name: 'user-parent',
-            //     title: 'api管理',
-            //     icon: shallowRef(User),
-            //     children: [
-            //         {
-            //             name: 'user',
-            //             title: '席位管理'
-            //         },
-            //         {
-            //             name: 'apiUser',
-            //             title: '用户信息'
-            //         },
-            //         {
-            //             name: 'usageDetail',
-            //             title: '用量统计'
-            //         }
-            //     ]
-            // },
             {
-                name: 'apiUserEdit',
+                name: 'orgEdit',
                 title: '企业信息',
-                icon: shallowRef(License)
-            },
-            {
-                name: 'user',
-                title: '席位管理',
-                icon: shallowRef(Users)
+                icon: Building
             },
             {
-                name: 'usageDetail',
-                title: '用量统计',
-                icon: shallowRef(Database)
-            },
-            {
-                name: 'library',
-                title: '行业知识库',
-                icon: shallowRef(Notebook)
+                name: 'knowledge',
+                title: '知识库管理',
+                icon: Notebook
             },
             {
-                name: 'librarySetting',
-                title: '知识库管理',
-                icon: shallowRef(Files)
+                name: 'orgUser',
+                title: '席位管理',
+                icon: Users
             },
             {
                 name: 'setting',
                 title: '系统管理',
-                icon: shallowRef(Settings)
+                icon: Settings
             }
         ]
     }
+    return []
 })
+
 function toggleMenu() {
     showDrawer.value = !showDrawer.value
 }

+ 75 - 0
src/views/OrgEditView.vue

@@ -0,0 +1,75 @@
+<template>
+    <ElForm :model="model" label-width="100px" label-position="right" ref="form" :rules="rules">
+        <ElFormItem prop="name" label="企业名称">
+            <ElInput v-model="model.name" placeholder="请输入企业名称" />
+        </ElFormItem>
+        <ElFormItem prop="logo" label="LOGO">
+            <SingleUpload v-model="model.logo" />
+        </ElFormItem>
+        <ElFormItem prop="assistantName" label="助手名称">
+            <ElInput v-model="model.assistantName" placeholder="请输入昵称" />
+        </ElFormItem>
+        <ElFormItem prop="description" label="描述">
+            <ElInput type="textarea" v-model="model.description" placeholder="请输入描述" />
+        </ElFormItem>
+        <ElFormItem prop="systemPrompt" label="系统提示词">
+            <ElInput type="textarea" v-model="model.systemPrompt" placeholder="请输入系统提示词" autosize />
+        </ElFormItem>
+        <ElFormItem prop="contextTemplate" label="上下文模版">
+            <ElInput type="textarea" v-model="model.contextTemplate" placeholder="请输入上下文模版" autosize />
+        </ElFormItem>
+        <ElFormItem prop="questionTemplate" label="问题模版">
+            <ElInput type="textarea" v-model="model.questionTemplate" placeholder="请输入问题模版" autosize />
+        </ElFormItem>
+        <ElFormItem prop="orgId" label="企业ID" v-if="model.roles && model.roles[0] === 'org'">
+            <ElInputNumber :controls="false" v-model="model.orgId" placeholder="请输入企业ID" />
+        </ElFormItem>
+        <ElFormItem>
+            <ElButton @click="save" :loading="saving" type="primary">保存</ElButton>
+        </ElFormItem>
+    </ElForm>
+</template>
+<script setup>
+import { onMounted, ref } from 'vue'
+import { http } from '@/plugins/http'
+import { ElMessage } from 'element-plus'
+const form = ref(null)
+const model = ref({})
+const rules = {
+    name: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
+    logo: [{ required: true, message: '请上传LOGO', trigger: 'blur' }],
+    assistantName: [{ required: true, message: '请输入助手名称', trigger: 'blur' }],
+    description: [{ required: true, message: '请输入描述', trigger: 'blur' }],
+    systemPrompt: [{ required: true, message: '请输入系统提示词', trigger: 'blur' }],
+}
+const saving = ref(false)
+onMounted(() => {
+    http.get('/org/my').then((res) => {
+        model.value = res
+    })
+})
+async function save() {
+    try {
+        try {
+            await form.value.validate()
+        } catch (e) {
+            return
+        }
+        saving.value = true
+        await http.put('/org/my', model.value)
+        saving.value = false
+        ElMessage.success('保存成功')
+    } catch (e) {
+        saving.value = false
+        if (e.message) {
+            if (typeof e.message === 'string') {
+                ElMessage.error(e.message)
+            } else {
+                ElMessage.error(JSON.stringify(e.message))
+            }
+        } else {
+            ElMessage.error('保存失败')
+        }
+    }
+}
+</script>

+ 69 - 0
src/views/OrgUserView.vue

@@ -0,0 +1,69 @@
+<template>
+    <PagingTable url="/admin/org/users" :where="where" ref="table">
+        <template #filter>
+            <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
+        </template>
+        <ElTableColumn prop="id" label="#" width="80" />
+        <ElTableColumn prop="name" label="姓名" min-width="120" />
+        <ElTableColumn prop="phone" label="手机" min-width="120" />
+        <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
+        <ElTableColumn label="操作" align="center" width="160">
+            <template #default="{ row }">
+                <ElButton @click="deleteRow(row)">删除</ElButton>
+            </template>
+        </ElTableColumn>
+    </PagingTable>
+    <EditDialog v-model="showEditDialog" :model="model" :rules="rules" :on-submit="submit" @success="table.refresh()">
+        <ElFormItem prop="name" label="姓名">
+            <ElInput v-model="model.name" placeholder="请输入昵称" />
+        </ElFormItem>
+        <ElFormItem prop="phone" label="手机">
+            <ElInput v-model="model.phone" placeholder="请输入手机" />
+        </ElFormItem>
+    </EditDialog>
+</template>
+<script setup>
+import { ref } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useTimeFormatter } from '@/utils/formatter'
+import { Plus } from '@vicons/tabler'
+import EditDialog from '@/components/EditDialog.vue'
+import { setupEditDialog } from '@/utils/editDialog'
+import EnumSelect from '@/components/EnumSelect.vue'
+import { UserRole } from '@/enums'
+import { http } from '@/plugins/http'
+import { ElMessage } from 'element-plus'
+import { useClipboard } from '@vueuse/core'
+import { ElMessageBox } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import { useUserStore } from '@/stores/user'
+
+const { user } = storeToRefs(useUserStore())
+const where = ref({})
+const timeFormatter = useTimeFormatter()
+const table = ref(null)
+const model = ref({})
+const rules = {
+    name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+    phone: [{ required: true, message: '请输入手机', trigger: 'blur' }],
+    orgId: [{ required: true, message: '请输入企业ID', trigger: 'blur' }]
+}
+const { showEditDialog, onEdit } = setupEditDialog(model)
+async function submit() {
+    model.value.orgId = user.value.orgId
+    await http.put('/admin/org/users', [model.value])
+    ElMessage.success('保存成功')
+}
+async function deleteRow(row) {
+    try {
+        await ElMessageBox.confirm('此操作将永久删除数据, 是否继续?', '提示', {
+            type: 'warning'
+        })
+        await http.delete(`/admin/org/users/${row.id}`)
+        ElMessage.success('删除成功')
+        table.value.refresh()
+    } catch (error) {
+        if ('cancel' !== error) ElMessage.error(error.message)
+    }
+}
+</script>

+ 100 - 0
src/views/OrgView.vue

@@ -0,0 +1,100 @@
+<template>
+    <PagingTable url="/admin/org" :where="where" ref="table">
+        <template #filter>
+            <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
+        </template>
+        <ElTableColumn prop="id" label="#" width="80" />
+        <ElTableColumn prop="name" label="企业名称" min-width="120" />
+        <ElTableColumn prop="logo" label="LOGO" min-width="80" align="center">
+            <template #default="{ row }">
+                <div class="flex items-center justify-center">
+                    <ElImage :src="row.logo" v-if="row.logo" style="width: 30px; height: 30px" fit="cover"></ElImage>
+                </div>
+            </template>
+        </ElTableColumn>
+        <ElTableColumn prop="assistantName" label="助手名称" min-width="120" />
+        <ElTableColumn prop="description" label="描述" min-width="120" show-overflow-tooltip></ElTableColumn>
+        <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
+        <ElTableColumn label="操作" align="center" width="100">
+            <template #default="{ row }">
+                <ElButton @click="onEdit(row)">编辑</ElButton>
+            </template>
+        </ElTableColumn>
+    </PagingTable>
+    <EditDialog
+        v-model="showEditDialog"
+        :model="model"
+        :rules="rules"
+        :on-submit="submit"
+        @success="table.refresh()"
+        label-width="100px"
+    >
+        <ElFormItem prop="name" label="企业名称">
+            <ElInput v-model="model.name" placeholder="请输入企业名称" />
+        </ElFormItem>
+        <ElFormItem prop="logo" label="LOGO">
+            <SingleUpload v-model="model.logo" />
+        </ElFormItem>
+        <ElFormItem prop="assistantName" label="助手名称">
+            <ElInput v-model="model.assistantName" placeholder="请输入昵称" />
+        </ElFormItem>
+        <ElFormItem prop="description" label="描述">
+            <ElInput type="textarea" v-model="model.description" placeholder="请输入描述" autosize />
+        </ElFormItem>
+        <ElFormItem prop="systemPrompt" label="系统提示词">
+            <ElInput type="textarea" v-model="model.systemPrompt" placeholder="请输入系统提示词" autosize />
+        </ElFormItem>
+        <ElFormItem prop="contextTemplate" label="上下文模版">
+            <ElInput type="textarea" v-model="model.contextTemplate" placeholder="请输入上下文模版" autosize />
+        </ElFormItem>
+        <ElFormItem prop="questionTemplate" label="问题模版">
+            <ElInput type="textarea" v-model="model.questionTemplate" placeholder="请输入问题模版" autosize />
+        </ElFormItem>
+        <ElFormItem prop="subdomain" label="子域名">
+            <ElInput v-model="model.subdomain" placeholder="请输入子域名" />
+        </ElFormItem>
+        <ElFormItem prop="customDomain" label="自定义域名">
+            <ElInput v-model="model.customDomain" placeholder="请输入自定义域名" />
+        </ElFormItem>
+        <ElFormItem prop="orgId" label="企业ID" v-if="model.roles && model.roles[0] === 'org'">
+            <ElInputNumber :controls="false" v-model="model.orgId" placeholder="请输入企业ID" />
+        </ElFormItem>
+    </EditDialog>
+</template>
+<script setup>
+import { ref } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useTimeFormatter } from '@/utils/formatter'
+import { Plus } from '@vicons/tabler'
+import EditDialog from '@/components/EditDialog.vue'
+import { setupEditDialog } from '@/utils/editDialog'
+import EnumSelect from '@/components/EnumSelect.vue'
+import { http } from '@/plugins/http'
+import { ElMessage } from 'element-plus'
+import { useClipboard } from '@vueuse/core'
+import SingleUpload from '@/components/SingleUpload.vue'
+
+const where = ref({})
+const timeFormatter = useTimeFormatter()
+const table = ref(null)
+const model = ref({})
+const rules = {
+    name: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
+    logo: [{ required: true, message: '请上传LOGO', trigger: 'blur' }],
+    assistantName: [{ required: true, message: '请输入助手名称', trigger: 'blur' }],
+    description: [{ required: true, message: '请输入描述', trigger: 'blur' }],
+    systemPrompt: [{ required: true, message: '请输入系统提示词', trigger: 'blur' }]
+}
+const { showEditDialog, onEdit } = setupEditDialog(model)
+async function submit() {
+    await http.put(model.value.id ? `/admin/org/${model.value.id}` : '/admin/org', model.value)
+    ElMessage.success('保存成功')
+}
+function getToken(row) {
+    http.get(`/auth/admin/user/${row.id}/token`).then((res) => {
+        const { copy } = useClipboard({ legacy: true })
+        copy(res.access_token)
+        ElMessage.success('复制成功')
+    })
+}
+</script>

+ 15 - 26
src/views/UserView.vue

@@ -1,7 +1,7 @@
 <template>
-    <PagingTable url="/admin/users" :freeze="freeze" :where="where" ref="table">
+    <PagingTable url="/admin/users" :where="where" ref="table">
         <template #filter>
-            <EnumSelect :enum="UserRole" v-model="where.roles" v-if="role === 'admin'"></EnumSelect>
+            <EnumSelect :enum="UserRole" v-model="where.roles"></EnumSelect>
             <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
         </template>
         <ElTableColumn prop="id" label="#" width="80" />
@@ -10,9 +10,10 @@
         <ElTableColumn prop="phone" label="手机" min-width="120" />
         <ElTableColumn prop="createdAt" label="注册时间" :formatter="timeFormatter" width="150" />
         <ElTableColumn prop="invitor" label="上级" />
-        <ElTableColumn label="操作" align="center" width="120">
+        <ElTableColumn label="操作" align="center" width="160">
             <template #default="{ row }">
                 <ElButton @click="getToken(row)">Token</ElButton>
+                <ElButton @click="onEdit(row)">编辑</ElButton>
             </template>
         </ElTableColumn>
     </PagingTable>
@@ -26,16 +27,19 @@
         <ElFormItem prop="phone" label="手机">
             <ElInput v-model="model.phone" placeholder="请输入手机" />
         </ElFormItem>
-        <ElFormItem prop="password" label="密码">
+        <ElFormItem prop="password" label="密码" v-if="!model.id">
             <ElInput v-model="model.password" placeholder="请输入密码" />
         </ElFormItem>
         <ElFormItem prop="roles" label="角色">
             <EnumSelect v-model="model.roles" :enum="UserRole" multiple :multiple-limit="1" />
         </ElFormItem>
+        <ElFormItem prop="orgId" label="企业ID" v-if="model.roles && model.roles[0] === 'org'">
+            <ElInputNumber :controls="false" v-model="model.orgId" placeholder="请输入企业ID" />
+        </ElFormItem>
     </EditDialog>
 </template>
 <script setup>
-import { nextTick, ref } from 'vue'
+import { ref } from 'vue'
 import PagingTable from '@/components/PagingTable.vue'
 import { useTimeFormatter } from '@/utils/formatter'
 import { Plus } from '@vicons/tabler'
@@ -51,35 +55,20 @@ const where = ref({ roles: 'user' })
 const timeFormatter = useTimeFormatter()
 const table = ref(null)
 const model = ref({})
-let role = 'admin'
-const freeze = ref(true)
-http.get(`/auth/admin/getRole`).then((res) => {
-    if (res === 'api') {
-        http.get('/admin/users/get').then((res) => {
-            where.value = { apiUserId: res.apiUserId, roles: 'api' }
-            role = 'api'
-            freeze.value = false
-            nextTick(() => {
-                table.value.refresh()
-            })
-        })
-    } else {
-        freeze.value = false
-        nextTick(() => {
-            table.value.refresh()
-        })
-    }
-})
 const rules = {
     username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
     name: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
     phone: [{ required: true, message: '请输入手机', trigger: 'blur' }],
     password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
-    roles: [{ required: true, message: '请选择角色', trigger: 'blur' }]
+    roles: [{ required: true, message: '请选择角色', trigger: 'blur' }],
+    orgId: [{ required: true, message: '请输入企业ID', trigger: 'blur' }]
 }
 const { showEditDialog, onEdit } = setupEditDialog(model)
 async function submit() {
-    await http.put('/admin/users', model.value)
+    if (model.value.roles[0] !== 'org') {
+        model.value.orgId = null
+    }
+    await http.put(model.value.id ? `/admin/users/${model.value.id}` : '/admin/users', model.value)
     ElMessage.success('保存成功')
 }
 function getToken(row) {

+ 5 - 0
yarn.lock

@@ -1741,6 +1741,11 @@ resolve-from@^4.0.0:
   resolved "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
+resolve-url@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.npmmirror.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+  integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==
+
 resolve@^1.1.7, resolve@^1.22.1:
   version "1.22.2"
   resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f"