index.vue 14.4 KB
<template>
    <Head title="音频上传 - 泡泡留声"/>

    <a-layout style="height: 100vh;width: 100vw">
        <a-layout-header class="header">
            <div class="content">
                <img class="logo" alt="logo" :src="logoSrc">
                <div class="title">泡泡留声</div>
                <img class="intro" alt="intro"
                     src="https://hising-cdn.hikoon.com/file/20231218/vxdqg9e0wum1702878136654ca1v3xg6w5d.png"/>
            </div>
        </a-layout-header>
        <a-layout-content class="main">
            <a-card ref="contentRef" :bordered="false" style="overflow: hidden;padding: 20px"
                    :body-style="{ display: 'flex',flexWarp:'nowrap',justifyContent:'center',padding:0,height:'65vh' }">
                <a-upload ref="uploadRef" :accept="uploadAccept" class="left" response-url-key="url"
                          :auto-upload="true" :draggable="true" :show-file-list="false" :disabled="!!uploadFile"
                          :custom-request="uploadRequest" :on-before-upload="onBefore">
                    <template #upload-button>
                        <a-layout style="height: 100%;">
                            <a-layout-header class="top">
                                <div style="font-size: 20px;font-weight: bold">音频文件上传</div>
                                <div v-if="isUploadSuccess" style="margin-right: 1em">
                                    <a-button type="primary" status="danger" @click.stop="onReUpload">继续上传
                                    </a-button>
                                    <a-button class="close-btn" type="primary" @click.stop="onClose">关闭</a-button>
                                </div>
                            </a-layout-header>
                            <a-layout-content class="middle">
                                <template v-if="isUploading">
                                    <div style="font-size: 16px;margin-bottom: 1em;color: #4E5969;">
                                        {{ floor(uploadFile?.percent || 0, 2) * 100 }}%
                                    </div>
                                    <a-progress style="width: 50%" :show-text="false" size="large"
                                                :percent="uploadFile?.percent"/>
                                    <div style="font-size: 16px;margin-top: 1em;color: #4E5969;">
                                        文件上传中 ...
                                    </div>
                                </template>
                                <template v-else-if="isUploadError">
                                    <div style="display: flex;align-items: center">
                                        <icon-exclamation-circle-fill style="color: #D62926" :size="25"/>
                                        <div style="font-size: 16px;color: #666666;margin-left: 10px">
                                            文件上传失败:网络连接超时
                                        </div>
                                    </div>
                                    <div style="font-size: 14px;margin-top: 1em;color: #999999;">
                                        您可以选择
                                        <span class="action" @click="onRetry">重试</span>

                                        <span class="action" @click.stop="onCancel">取消上传</span>
                                    </div>
                                </template>
                                <template v-else-if="isUploadSuccess">
                                    <div style="display: flex;align-items: center">
                                        <icon-check-circle-fill style="color: #0a7350" :size="25"/>
                                        <div style="font-size: 16px;color: #666666;margin-left: 10px">
                                            文件成功
                                        </div>
                                    </div>
                                    <div style="width: 52%;position: absolute;bottom: 40px;overflow: hidden">
                                        <audio-tool :name="uploadFile?.name" :url="uploadFile?.url as string"/>

                                        <div style="font-size: 14px;margin-top: 1em;color: #999999;">
                                            如确认音频无误,请点击通过【扫码上传】,识别右侧二维码
                                        </div>
                                    </div>
                                </template>
                                <template v-else>
                                    <img :width="80" :height="80" alt=""
                                         src="https://hising-cdn.hikoon.com/file/20231218/e1v0latccea1702891747759kklfhqv7a1o.png"/>
                                    <div style="font-size: 16px;margin-top: 1em;color: #4E5969;">
                                        拖拽文件到框内,或点击
                                        <span style="color: #D62926;border-bottom: 1px solid #D62926">上传</span>
                                    </div>
                                </template>
                            </a-layout-content>
                        </a-layout>
                    </template>
                </a-upload>
                <div class="right">
                    <div class="info">
                        <div v-if="isUploadSuccess">
                            <vue-qr :size="150" :margin="5" :logo-margin="3" :logo-scale="0.2" :logo-src="logoSrc"
                                    :text="uploadFileEncrypt" :correct-level="3"/>
                            <a-typography-paragraph type="secondary" style="margin-bottom: 0;margin-top: 10px">
                                <icon-check-circle-fill style="color: #0a7350"/>
                                请在应用中通过[扫码上传]识别
                            </a-typography-paragraph>
                        </div>
                        <div v-else>
                            <a-progress v-if="isUploading" size="medium" type="circle" :percent="uploadFile?.percent"
                                        style="margin-bottom: 20px"/>
                            <a-typography-paragraph type="secondary">文件上传成功后</a-typography-paragraph>
                            <a-typography-paragraph style="margin-bottom: 0" type="secondary">
                                即可生成二维码
                            </a-typography-paragraph>
                        </div>
                    </div>
                    <a-typography>
                        <a-typography-title :heading="6">操作说明:</a-typography-title>
                        <a-typography-paragraph type="secondary">
                            1、将电脑上的【试唱音频】或【其它音频文件】通过网页上传,生成二维码
                        </a-typography-paragraph>
                        <a-typography-paragraph type="secondary">
                            2、在【泡泡留声】应用中点击【扫码上传】识别上方二维码即可
                        </a-typography-paragraph>
                    </a-typography>
                    <div class="customer">
                        <img alt="" :width="110" :height="110" :src="customerSrc"/>
                        <div>问题咨询及反馈,请联系客服</div>
                    </div>
                </div>
            </a-card>
        </a-layout-content>
        <a-layout-footer class="footer">
            Copyright &copy 2023 北京海葵科技有限公司. All Rights Reserved.
            <a class="link" href="https://beian.miit.gov.cn/" target="_blank">京ICP备 17046250号</a>
            <a class="link" href="https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=11011202000841"
               target="_blank">
                京公网安备 11011202000841号
            </a>
        </a-layout-footer>
    </a-layout>

    <login-modal :logo="logoSrc" :width="300"/>
</template>

<script lang="ts" setup>
import LoginModal from "@/page/upload/components/login-modal.vue";
import AudioTool from "@/page/upload/components/audio-tool.vue";

import {computed, ref} from "vue";
import {Head, usePage} from '@inertiajs/vue3'
import {useElementSize, useLocalStorage} from '@vueuse/core'

import {floor} from "lodash-es";
import OSS from "ali-oss";
import axios from "axios";
import {v4 as uuid4} from "uuid";
import {FileItem, Modal, RequestOption, Upload} from "@arco-design/web-vue";
import vueQr from 'vue-qr/src/packages/vue-qr.vue'

import {Base64} from "js-base64";

const {logoSrc, customerSrc, uploadAccept, ossConfig}: any = usePage().props

const contentRef = ref()
const {width: headWidth} = useElementSize(contentRef)


const uploadRef = ref<InstanceType<typeof Upload>>()
const uploadFile = ref<FileItem>()
const uploadHover = computed(() => uploadFile.value ? 'unset' : 'pointer')

const uploadClient = new OSS({
    region: ossConfig.region,
    bucket: ossConfig.bucket,
    accessKeyId: ossConfig.accessKeyId,
    accessKeySecret: ossConfig.accessKeySecret,
    stsToken: ossConfig.stsToken,
    refreshSTSTokenInterval: 3000000,
    refreshSTSToken: async () => {
        const {data} = await axios.post('/app/upload-sts-token')
        return {accessKeyId: data.accessKeyId, accessKeySecret: data.accessKeySecret, stsToken: data.stsToken}
    }
})

const isUploading = computed(() => uploadFile.value?.status === 'uploading')
const isUploadSuccess = computed(() => uploadFile.value?.status === 'done')
const isUploadError = computed(() => uploadFile.value?.status === 'error')

const uploadFileEncrypt = computed((): string =>
    Base64.encode(JSON.stringify({type: 'uploadFile', data: uploadFile.value?.response})))

const token = useLocalStorage('token', '', {initOnMounted: false})

axios.interceptors.request.use((config) => {
    config.headers.Authorization = token.value
    return config;
})

axios.interceptors.response.use((response) => {
    if (response.data.code === 401) {
        token.value = ''
        return Promise.reject(response.data)
    }
    return response.data;
})

const uploadRequest = (option: RequestOption) => {
    const {onProgress, onError, onSuccess, fileItem} = option

    uploadFile.value = fileItem

    const fileDir = new Date().toISOString().slice(0, 10).replace(/-/g, '');
    const fileType = fileItem.file?.name?.split('.')?.pop()?.toLowerCase();

    uploadClient.multipartUpload(`audio/${fileDir}/${uuid4()}.${fileType}`, fileItem.file, {
        progress: (p) => onProgress(Number(p.toFixed(2))),
        parallel: 4, partSize: 2 * 1024 * 1024, mime: fileItem.file?.type
    }).then(({name}) => onSuccess({
        name: fileItem.name,
        type: fileItem.file?.type,
        size: fileItem.file?.size,
        url: `${ossConfig.domain}/${name}`,
    })).catch(e => onError(e))

    return {}
}

const onBefore = (file: File) => {
    if (file.size > 100 * 1024 * 1024) {
        Modal.open({
            title: '温馨提示',
            content: '文件超过最大100M限制',
            okText: '我知道了',
            hideCancel: true, escToClose: false, maskClosable: false, closable: false,
        })

        return false;
    }

    return true;
}

const onCancel = () => {
    uploadFile.value = undefined
}

const onRetry = () => {
    uploadRef.value?.submit(uploadFile.value)
}

const onClose = () => Modal.open({
    title: '温馨提示',
    content: '确定关闭吗?',
    escToClose: false, maskClosable: false, closable: false,
    onOk: () => onCancel()
})

const onReUpload = () => Modal.open({
    title: '温馨提示',
    content: '已完成该文件上传,确定继续上传吗?',
    escToClose: false, maskClosable: false, closable: false,
    onOk: () => {
        uploadRef.value?.$el?.querySelector('input').removeAttribute('disabled')
        uploadRef.value?.$el?.querySelector('input').click()
    }
})
</script>

<style scoped>

.header {
    background-color: #1E1B29;
    color: #ffffff;
    padding: 0 20px;
    z-index: 1;

    .content {
        display: flex;
        align-items: center;
        justify-content: flex-start;
        height: 60px;
        width: v-bind(headWidth+ 'px');
        margin: auto;

        .logo {
            width: 32px;
            height: 32px;
            border-radius: 3px;
        }

        .title {
            margin-left: 8px;
            font-size: 15px;
            font-weight: 600;
            line-height: 18px;
        }

        .intro {
            height: 15px;
            margin-left: 15px;
        }
    }
}

.footer {
    height: 60px;
    background-color: #1E1B29;
    color: #7F7F7F;
    text-align: center;
    font-size: 12px;
    line-height: 60px;
    z-index: 1;

    .link {
        text-decoration: none;
        color: inherit;
    }
}

.main {
    display: flex;
    justify-content: center;
    align-items: center;
    overflow: auto;
    min-height: 540px;

    .left {
        width: 900px;
        background-color: #FAFAFA;
        cursor: v-bind(uploadHover);

        .top {
            height: 60px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0 32px;

            .close-btn {
                margin-left: 1em;
                background-color: #efefef;
                color: #666666;
            }
        }

        .middle {
            display: flex;
            flex-direction: column;
            flex-wrap: nowrap;
            align-content: center;
            justify-content: center;
            align-items: center;
            padding-bottom: 60px;

            .action {
                color: #666666;
                border-bottom: 1px solid #666666;

                &:hover {
                    cursor: pointer;
                }
            }
        }
    }

    .right {
        margin-left: 20px;
        height: 100%;
        display: flex;
        flex-direction: column;
        justify-content: space-between;

        .info {
            width: 80%;
            height: 215px;
            border: 1px solid #bdc1c6;
            text-align: center;
            display: flex;
            flex-direction: column;
            flex-wrap: nowrap;
            justify-content: center;
            align-items: center;
        }

        .customer {
            display: flex;
            align-items: center;

            :not(:first-child) {
                margin-left: 14px;
            }
        }
    }
}
</style>