xiongzhu 2 лет назад
Родитель
Сommit
19475fcf4a

+ 2 - 1
.eslintrc.cjs

@@ -11,6 +11,7 @@ module.exports = {
         'no-unused-vars': 'off'
     },
     globals: {
-        process: true
+        process: true,
+        AppInfoParser: true
     }
 }

+ 2 - 1
index.html

@@ -4,10 +4,11 @@
     <meta charset="UTF-8">
     <link rel="icon" href="/favicon.ico">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
-    <title>ChatAdmin管理后台</title>
+    <title>NbookAdmin</title>
   </head>
   <body>
     <div id="app"></div>
     <script type="module" src="/src/main.js"></script>
+    <script src="/app-info-parser.min.js"></script>
   </body>
 </html>

+ 1 - 0
package.json

@@ -17,6 +17,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"
   },

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
public/app-info-parser.min.js


+ 183 - 0
src/components/MultiUpload.vue

@@ -0,0 +1,183 @@
+<template>
+    <section>
+        <el-upload
+            list-type="picture-card"
+            :action="uploadUrl"
+            :headers="headers"
+            :on-preview="handlePictureCardPreview"
+            :on-remove="handleRemove"
+            :on-success="handleSuccess"
+            :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>
+        <el-dialog v-model="dialogVisible">
+            <img width="100%" :src="dialogImageUrl" alt />
+        </el-dialog>
+        <el-image
+            preview-teleported
+            style="width: 0; height: 0; position: absolute"
+            ref="preview"
+            :src="previewUrl"
+            :preview-src-list="previewList"
+        >
+        </el-image>
+    </section>
+</template>
+<script>
+import resolveUrl from 'resolve-url'
+export default {
+    created() {
+        this.uploadUrl = resolveUrl(this.$http.baseURL, 'upload/file')
+        this.updateFileList(this.modelValue)
+    },
+    props: {
+        modelValue: Array,
+        usePrefix: {
+            type: Boolean,
+            default: true
+        },
+        url: {
+            type: String
+        },
+        maxWidth: {
+            type: Number,
+            default: -1
+        },
+        maxHeight: {
+            type: Number,
+            default: -1
+        },
+        maxSize: {
+            type: Number,
+            default: 1024 * 1024
+        }
+    },
+    data() {
+        return {
+            dialogImageUrl: '',
+            dialogVisible: false,
+            fileList: [],
+            uploadUrl: '',
+            realFileList: [],
+            previewUrl: '',
+            previewList: []
+        }
+    },
+    computed: {
+        headers() {
+            return {
+                Authorization: 'Bearer ' + localStorage.getItem('token')
+            }
+        }
+    },
+    methods: {
+        handleRemove(file, fileList) {
+            this.realFileList = fileList
+            this.$emit(
+                'update:modelValue',
+                fileList.map((i) => i.value)
+            )
+        },
+        handlePictureCardPreview(file) {
+            this.previewUrl = file.value
+            this.previewList = this.fileList.map((i) => i.value)
+            this.$refs.preview.showViewer = true
+        },
+        handleSuccess(res, file, fileList) {
+            if (res instanceof Array) {
+                file.value = res[0]
+            } else {
+                file.value = res
+            }
+            this.realFileList = fileList
+            this.$emit(
+                'update:modelValue',
+                fileList.map((i) => i.value)
+            )
+        },
+        updateFileList(list) {
+            if (!list) {
+                list = []
+            } else if (typeof list == 'string') {
+                list = list.split(',')
+            }
+            for (let i = 0; i < list.length; i++) {
+                if (!this.fileList[i]) {
+                    this.fileList[i] = {
+                        value: list[i],
+                        url: list[i]
+                    }
+                } else if (this.fileList[i].value !== list[i]) {
+                    this.fileList[i] = {
+                        value: list[i],
+                        url: list[i]
+                    }
+                }
+            }
+            this.fileList.splice(list.length)
+            // this.fileList = list.map(i => {
+            //     return {
+            //         value: i,
+            //         url: i,
+            //     };
+            // });
+        },
+        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
+            }
+            return new Promise((resolve, reject) => {
+                let image = document.createElement('img')
+                image.onload = () => {
+                    resolve({
+                        width: image.width,
+                        height: image.height
+                    })
+                }
+                image.onerror = () => {
+                    this.$message.error('图片加载失败!')
+                    reject()
+                }
+                image.src = URL.createObjectURL(file)
+            }).then((res) => {
+                if (this.maxWidth > 0 && res.width > this.maxWidth) {
+                    this.$message.error('上传图片宽度不能超过 ' + this.maxWidth + 'px!')
+                    return Promise.reject()
+                } else if (this.maxHeight > 0 && res.height > this.maxHeight) {
+                    this.$message.error('上传图片高度不能超过 ' + this.maxHeight + 'px!')
+                    return Promise.reject()
+                } else {
+                    return Promise.resolve()
+                }
+            })
+        }
+    },
+    watch: {
+        modelValue: {
+            handler(val, oldVal) {
+                if (JSON.stringify(val) == JSON.stringify(oldVal)) {
+                    return
+                }
+                this.updateFileList(val)
+            },
+            deep: true
+        }
+    }
+}
+</script>
+<style lang="less" scoped></style>

+ 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>

+ 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
+    })
+}

+ 69 - 3
src/views/HomeView.vue

@@ -1,5 +1,71 @@
-<script setup></script>
-
 <template>
-    <el-main> </el-main>
+    <el-main>
+        <ElForm :model="model" label-position="top">
+            <ElFormItem label="安装包">
+                <ElUpload
+                    accept="application/vnd.android.package-archive"
+                    :action="uploadUrl"
+                    :show-file-list="false"
+                    :before-upload="beforeUpload"
+                    :on-success="onSuccess"
+                    :on-error="onError"
+                >
+                    <template v-if="model.appInfo.url">
+                        <br />
+                        <ElLink :href="model.appInfo.url" target="_blank">{{ model.appInfo.url }}</ElLink>
+                    </template>
+                    <template #trigger>
+                        <ElButton :loading="uploading">上传</ElButton>
+                    </template>
+                </ElUpload>
+            </ElFormItem>
+            <ElFormItem label="版本号">
+                <ElInput v-model="model.appInfo.package" disabled />
+            </ElFormItem>
+            <ElFormItem label="版本名">
+                <ElInput v-model="model.appInfo.versionName" disabled />
+            </ElFormItem>
+            <ElFormItem label="版本号">
+                <ElInput v-model="model.appInfo.versionCode" disabled />
+            </ElFormItem>
+            <ElFormItem label="开屏页">
+                <SingleUpload v-model="model.splash.url" />
+            </ElFormItem>
+        </ElForm>
+    </el-main>
 </template>
+
+<script setup>
+import { ref } from 'vue'
+import resolveUrl from 'resolve-url'
+import SingleUpload from '@/components/SingleUpload.vue'
+const uploadUrl = resolveUrl(import.meta.env.VITE_API_BASE_URL, '/api/file/upload')
+const model = ref({ appInfo: {}, splash: { enabled: true }, banner: { enabled: true } })
+console.log(uploadUrl)
+const uploading = ref(false)
+async function beforeUpload(file) {
+    const parser = new AppInfoParser(file)
+    try {
+        const info = await parser.parse()
+        console.log(info)
+        model.value.appInfo = {
+            package: info.package,
+            versionCode: info.versionCode,
+            versionName: info.versionName
+        }
+        uploading.value = true
+        return true
+    } catch (error) {
+        console.log(error)
+        return false
+    }
+}
+function onSuccess(e) {
+    uploading.value = false
+    model.value.appInfo.url = e.url
+}
+function onError(e) {
+    uploading.value = false
+    console.log(e)
+}
+</script>

+ 1 - 1
src/views/LoginView.vue

@@ -1,7 +1,7 @@
 <template>
     <ElContainer class="h-full bg-cover bg-center" @keyup.enter="login" :style="{ backgroundImage: `url(${bg})` }">
         <ElMain class="backdrop-blur1 dark:backdrop-brightness-50 !flex flex-col items-center justify-center">
-            <span class="text-3xl font-[sh] text-white [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">ChatAdmin</span>
+            <span class="text-3xl font-[sh] text-white [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">NbookAdmin</span>
             <ElCard class="w-full max-w-lg !rounded-xl mt-8 mb-16">
                 <ElForm :model="model" :rules="rules" label-position="top" ref="form">
                     <ElFormItem prop="username" label="用户名">

+ 1 - 1
src/views/MainView.vue

@@ -2,7 +2,7 @@
     <ElContainer class="h-full">
         <ElAside class="bg-slate-100 dark:bg-zinc-800" v-if="!isMobile">
             <div class="h-16 px-4 flex items-center justify-center cursor-pointer">
-                <span class="text-lg font-[sh]">ChatAdmin</span>
+                <span class="text-lg font-[sh]">NbookAdmin</span>
             </div>
             <SideMenu :default-active="activeMenu" :menus="menus" />
         </ElAside>

+ 3 - 0
vite.config.js

@@ -27,6 +27,9 @@ export default defineConfig(({ command, mode }) => {
         },
         server: {
             host: '0.0.0.0'
+        },
+        define: {
+            'process.env': process.env
         }
     }
 })

+ 5 - 0
yarn.lock

@@ -1728,6 +1728,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"

Некоторые файлы не были показаны из-за большого количества измененных файлов