mirror of
https://github.com/fumiama/paper-manager.git
synced 2026-06-29 23:30:43 +08:00
优化 filelist, add settings
This commit is contained in:
22
frontend/vben/src/api/sys/upload.ts
Normal file
22
frontend/vben/src/api/sys/upload.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { UploadApiResult } from './model/uploadModel'
|
||||||
|
import { defHttp } from '/@/utils/http/axios'
|
||||||
|
import { UploadFileParams } from '/#/axios'
|
||||||
|
import { useGlobSetting } from '/@/hooks/setting'
|
||||||
|
|
||||||
|
const { uploadUrl = '' } = useGlobSetting()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Upload interface
|
||||||
|
*/
|
||||||
|
export function uploadApi(
|
||||||
|
params: UploadFileParams,
|
||||||
|
onUploadProgress: (progressEvent: ProgressEvent) => void,
|
||||||
|
) {
|
||||||
|
return defHttp.uploadFile<UploadApiResult>(
|
||||||
|
{
|
||||||
|
url: uploadUrl,
|
||||||
|
onUploadProgress,
|
||||||
|
},
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
}
|
||||||
7
frontend/vben/src/components/Cropper/index.ts
Normal file
7
frontend/vben/src/components/Cropper/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { withInstall } from '/@/utils'
|
||||||
|
import cropperImage from './src/Cropper.vue'
|
||||||
|
import avatarCropper from './src/CropperAvatar.vue'
|
||||||
|
|
||||||
|
export * from './src/typing'
|
||||||
|
export const CropperImage = withInstall(cropperImage)
|
||||||
|
export const CropperAvatar = withInstall(avatarCropper)
|
||||||
284
frontend/vben/src/components/Cropper/src/CopperModal.vue
Normal file
284
frontend/vben/src/components/Cropper/src/CopperModal.vue
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
v-bind="$attrs"
|
||||||
|
@register="register"
|
||||||
|
:title="t('component.cropper.modalTitle')"
|
||||||
|
width="800px"
|
||||||
|
:canFullscreen="false"
|
||||||
|
@ok="handleOk"
|
||||||
|
:okText="t('component.cropper.okText')"
|
||||||
|
>
|
||||||
|
<div :class="prefixCls">
|
||||||
|
<div :class="`${prefixCls}-left`">
|
||||||
|
<div :class="`${prefixCls}-cropper`">
|
||||||
|
<CropperImage
|
||||||
|
v-if="src"
|
||||||
|
:src="src"
|
||||||
|
height="300px"
|
||||||
|
:circled="circled"
|
||||||
|
@cropend="handleCropend"
|
||||||
|
@ready="handleReady"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="`${prefixCls}-toolbar`">
|
||||||
|
<Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload">
|
||||||
|
<Tooltip :title="t('component.cropper.selectImage')" placement="bottom">
|
||||||
|
<a-button size="small" preIcon="ant-design:upload-outlined" type="primary" />
|
||||||
|
</Tooltip>
|
||||||
|
</Upload>
|
||||||
|
<Space>
|
||||||
|
<Tooltip :title="t('component.cropper.btn_reset')" placement="bottom">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
preIcon="ant-design:reload-outlined"
|
||||||
|
size="small"
|
||||||
|
:disabled="!src"
|
||||||
|
@click="handlerToolbar('reset')"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :title="t('component.cropper.btn_rotate_left')" placement="bottom">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
preIcon="ant-design:rotate-left-outlined"
|
||||||
|
size="small"
|
||||||
|
:disabled="!src"
|
||||||
|
@click="handlerToolbar('rotate', -45)"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :title="t('component.cropper.btn_rotate_right')" placement="bottom">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
preIcon="ant-design:rotate-right-outlined"
|
||||||
|
size="small"
|
||||||
|
:disabled="!src"
|
||||||
|
@click="handlerToolbar('rotate', 45)"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :title="t('component.cropper.btn_scale_x')" placement="bottom">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
preIcon="vaadin:arrows-long-h"
|
||||||
|
size="small"
|
||||||
|
:disabled="!src"
|
||||||
|
@click="handlerToolbar('scaleX')"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :title="t('component.cropper.btn_scale_y')" placement="bottom">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
preIcon="vaadin:arrows-long-v"
|
||||||
|
size="small"
|
||||||
|
:disabled="!src"
|
||||||
|
@click="handlerToolbar('scaleY')"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :title="t('component.cropper.btn_zoom_in')" placement="bottom">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
preIcon="ant-design:zoom-in-outlined"
|
||||||
|
size="small"
|
||||||
|
:disabled="!src"
|
||||||
|
@click="handlerToolbar('zoom', 0.1)"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :title="t('component.cropper.btn_zoom_out')" placement="bottom">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
preIcon="ant-design:zoom-out-outlined"
|
||||||
|
size="small"
|
||||||
|
:disabled="!src"
|
||||||
|
@click="handlerToolbar('zoom', -0.1)"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="`${prefixCls}-right`">
|
||||||
|
<div :class="`${prefixCls}-preview`">
|
||||||
|
<img :src="previewSource" v-if="previewSource" :alt="t('component.cropper.preview')" />
|
||||||
|
</div>
|
||||||
|
<template v-if="previewSource">
|
||||||
|
<div :class="`${prefixCls}-group`">
|
||||||
|
<Avatar :src="previewSource" size="large" />
|
||||||
|
<Avatar :src="previewSource" :size="48" />
|
||||||
|
<Avatar :src="previewSource" :size="64" />
|
||||||
|
<Avatar :src="previewSource" :size="80" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { CropendResult, Cropper } from './typing'
|
||||||
|
|
||||||
|
import { defineComponent, ref } from 'vue'
|
||||||
|
import CropperImage from './Cropper.vue'
|
||||||
|
import { Space, Upload, Avatar, Tooltip } from 'ant-design-vue'
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign'
|
||||||
|
import { BasicModal, useModalInner } from '/@/components/Modal'
|
||||||
|
import { dataURLtoBlob } from '/@/utils/file/base64Conver'
|
||||||
|
import { isFunction } from '/@/utils/is'
|
||||||
|
import { useI18n } from '/@/hooks/web/useI18n'
|
||||||
|
|
||||||
|
type apiFunParams = { file: Blob; name: string; filename: string }
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
circled: { type: Boolean, default: true },
|
||||||
|
uploadApi: {
|
||||||
|
type: Function as PropType<(params: apiFunParams) => Promise<any>>,
|
||||||
|
},
|
||||||
|
src: { type: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'CropperModal',
|
||||||
|
components: { BasicModal, Space, CropperImage, Upload, Avatar, Tooltip },
|
||||||
|
props,
|
||||||
|
emits: ['uploadSuccess', 'register'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
let filename = ''
|
||||||
|
const src = ref(props.src || '')
|
||||||
|
const previewSource = ref('')
|
||||||
|
const cropper = ref<Cropper>()
|
||||||
|
let scaleX = 1
|
||||||
|
let scaleY = 1
|
||||||
|
|
||||||
|
const { prefixCls } = useDesign('cropper-am')
|
||||||
|
const [register, { closeModal, setModalProps }] = useModalInner()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// Block upload
|
||||||
|
function handleBeforeUpload(file: File) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
src.value = ''
|
||||||
|
previewSource.value = ''
|
||||||
|
reader.onload = function (e) {
|
||||||
|
src.value = (e.target?.result as string) ?? ''
|
||||||
|
filename = file.name
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCropend({ imgBase64 }: CropendResult) {
|
||||||
|
previewSource.value = imgBase64
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReady(cropperInstance: Cropper) {
|
||||||
|
cropper.value = cropperInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlerToolbar(event: string, arg?: number) {
|
||||||
|
if (event === 'scaleX') {
|
||||||
|
scaleX = arg = scaleX === -1 ? 1 : -1
|
||||||
|
}
|
||||||
|
if (event === 'scaleY') {
|
||||||
|
scaleY = arg = scaleY === -1 ? 1 : -1
|
||||||
|
}
|
||||||
|
cropper?.value?.[event]?.(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOk() {
|
||||||
|
const uploadApi = props.uploadApi
|
||||||
|
if (uploadApi && isFunction(uploadApi)) {
|
||||||
|
const blob = dataURLtoBlob(previewSource.value)
|
||||||
|
try {
|
||||||
|
setModalProps({ confirmLoading: true })
|
||||||
|
const result = await uploadApi({ name: 'file', file: blob, filename })
|
||||||
|
emit('uploadSuccess', { source: previewSource.value, data: result.url })
|
||||||
|
closeModal()
|
||||||
|
} finally {
|
||||||
|
setModalProps({ confirmLoading: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
t,
|
||||||
|
prefixCls,
|
||||||
|
src,
|
||||||
|
register,
|
||||||
|
previewSource,
|
||||||
|
handleBeforeUpload,
|
||||||
|
handleCropend,
|
||||||
|
handleReady,
|
||||||
|
handlerToolbar,
|
||||||
|
handleOk,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@prefix-cls: ~'@{namespace}-cropper-am';
|
||||||
|
|
||||||
|
.@{prefix-cls} {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&-left,
|
||||||
|
&-right {
|
||||||
|
height: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-left {
|
||||||
|
width: 55%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-right {
|
||||||
|
width: 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-cropper {
|
||||||
|
height: 300px;
|
||||||
|
background: #eee;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgb(0 0 0 / 25%) 25%,
|
||||||
|
transparent 0,
|
||||||
|
transparent 75%,
|
||||||
|
rgb(0 0 0 / 25%) 0
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgb(0 0 0 / 25%) 25%,
|
||||||
|
transparent 0,
|
||||||
|
transparent 75%,
|
||||||
|
rgb(0 0 0 / 25%) 0
|
||||||
|
);
|
||||||
|
background-position: 0 0, 12px 12px;
|
||||||
|
background-size: 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-preview {
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
margin: 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid @border-color-base;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-group {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border-top: 1px solid @border-color-base;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
188
frontend/vben/src/components/Cropper/src/Cropper.vue
Normal file
188
frontend/vben/src/components/Cropper/src/Cropper.vue
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="getClass" :style="getWrapperStyle">
|
||||||
|
<img
|
||||||
|
v-show="isReady"
|
||||||
|
ref="imgElRef"
|
||||||
|
:src="src"
|
||||||
|
:alt="alt"
|
||||||
|
:crossorigin="crossorigin"
|
||||||
|
:style="getImageStyle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { CSSProperties } from 'vue'
|
||||||
|
import { defineComponent, onMounted, ref, unref, computed, onUnmounted } from 'vue'
|
||||||
|
import Cropper from 'cropperjs'
|
||||||
|
import 'cropperjs/dist/cropper.css'
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign'
|
||||||
|
import { useDebounceFn } from '@vueuse/shared'
|
||||||
|
|
||||||
|
type Options = Cropper.Options
|
||||||
|
|
||||||
|
const defaultOptions: Options = {
|
||||||
|
aspectRatio: 1,
|
||||||
|
zoomable: true,
|
||||||
|
zoomOnTouch: true,
|
||||||
|
zoomOnWheel: true,
|
||||||
|
cropBoxMovable: true,
|
||||||
|
cropBoxResizable: true,
|
||||||
|
toggleDragModeOnDblclick: true,
|
||||||
|
autoCrop: true,
|
||||||
|
background: true,
|
||||||
|
highlight: true,
|
||||||
|
center: true,
|
||||||
|
responsive: true,
|
||||||
|
restore: true,
|
||||||
|
checkCrossOrigin: true,
|
||||||
|
checkOrientation: true,
|
||||||
|
scalable: true,
|
||||||
|
modal: true,
|
||||||
|
guides: true,
|
||||||
|
movable: true,
|
||||||
|
rotatable: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
src: { type: String, required: true },
|
||||||
|
alt: { type: String },
|
||||||
|
circled: { type: Boolean, default: false },
|
||||||
|
realTimePreview: { type: Boolean, default: true },
|
||||||
|
height: { type: [String, Number], default: '360px' },
|
||||||
|
crossorigin: {
|
||||||
|
type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
|
||||||
|
options: { type: Object as PropType<Options>, default: () => ({}) },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'CropperImage',
|
||||||
|
props,
|
||||||
|
emits: ['cropend', 'ready', 'cropendError'],
|
||||||
|
setup(props, { attrs, emit }) {
|
||||||
|
const imgElRef = ref<ElRef<HTMLImageElement>>()
|
||||||
|
const cropper = ref<Nullable<Cropper>>()
|
||||||
|
const isReady = ref(false)
|
||||||
|
|
||||||
|
const { prefixCls } = useDesign('cropper-image')
|
||||||
|
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80)
|
||||||
|
|
||||||
|
const getImageStyle = computed((): CSSProperties => {
|
||||||
|
return {
|
||||||
|
height: props.height,
|
||||||
|
maxWidth: '100%',
|
||||||
|
...props.imageStyle,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const getClass = computed(() => {
|
||||||
|
return [
|
||||||
|
prefixCls,
|
||||||
|
attrs.class,
|
||||||
|
{
|
||||||
|
[`${prefixCls}--circled`]: props.circled,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const getWrapperStyle = computed((): CSSProperties => {
|
||||||
|
return { height: `${props.height}`.replace(/px/, '') + 'px' }
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(init)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cropper.value?.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const imgEl = unref(imgElRef)
|
||||||
|
if (!imgEl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cropper.value = new Cropper(imgEl, {
|
||||||
|
...defaultOptions,
|
||||||
|
ready: () => {
|
||||||
|
isReady.value = true
|
||||||
|
realTimeCroppered()
|
||||||
|
emit('ready', cropper.value)
|
||||||
|
},
|
||||||
|
crop() {
|
||||||
|
debounceRealTimeCroppered()
|
||||||
|
},
|
||||||
|
zoom() {
|
||||||
|
debounceRealTimeCroppered()
|
||||||
|
},
|
||||||
|
cropmove() {
|
||||||
|
debounceRealTimeCroppered()
|
||||||
|
},
|
||||||
|
...props.options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real-time display preview
|
||||||
|
function realTimeCroppered() {
|
||||||
|
props.realTimePreview && croppered()
|
||||||
|
}
|
||||||
|
|
||||||
|
// event: return base64 and width and height information after cropping
|
||||||
|
function croppered() {
|
||||||
|
if (!cropper.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let imgInfo = cropper.value.getData()
|
||||||
|
const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas()
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (!blob) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let fileReader: FileReader = new FileReader()
|
||||||
|
fileReader.readAsDataURL(blob)
|
||||||
|
fileReader.onloadend = (e) => {
|
||||||
|
emit('cropend', {
|
||||||
|
imgBase64: e.target?.result ?? '',
|
||||||
|
imgInfo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fileReader.onerror = () => {
|
||||||
|
emit('cropendError')
|
||||||
|
}
|
||||||
|
}, 'image/png')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a circular picture canvas
|
||||||
|
function getRoundedCanvas() {
|
||||||
|
const sourceCanvas = cropper.value!.getCroppedCanvas()
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const context = canvas.getContext('2d')!
|
||||||
|
const width = sourceCanvas.width
|
||||||
|
const height = sourceCanvas.height
|
||||||
|
canvas.width = width
|
||||||
|
canvas.height = height
|
||||||
|
context.imageSmoothingEnabled = true
|
||||||
|
context.drawImage(sourceCanvas, 0, 0, width, height)
|
||||||
|
context.globalCompositeOperation = 'destination-in'
|
||||||
|
context.beginPath()
|
||||||
|
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true)
|
||||||
|
context.fill()
|
||||||
|
return canvas
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getClass, imgElRef, getWrapperStyle, getImageStyle, isReady, croppered }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
@prefix-cls: ~'@{namespace}-cropper-image';
|
||||||
|
|
||||||
|
.@{prefix-cls} {
|
||||||
|
&--circled {
|
||||||
|
.cropper-view-box,
|
||||||
|
.cropper-face {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
161
frontend/vben/src/components/Cropper/src/CropperAvatar.vue
Normal file
161
frontend/vben/src/components/Cropper/src/CropperAvatar.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="getClass" :style="getStyle">
|
||||||
|
<div :class="`${prefixCls}-image-wrapper`" :style="getImageWrapperStyle" @click="openModal">
|
||||||
|
<div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle">
|
||||||
|
<Icon
|
||||||
|
icon="ant-design:cloud-upload-outlined"
|
||||||
|
:size="getIconWidth"
|
||||||
|
:style="getImageWrapperStyle"
|
||||||
|
color="#d6d6d6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<img :src="sourceValue" v-if="sourceValue" alt="avatar" />
|
||||||
|
</div>
|
||||||
|
<a-button
|
||||||
|
:class="`${prefixCls}-upload-btn`"
|
||||||
|
@click="openModal"
|
||||||
|
v-if="showBtn"
|
||||||
|
v-bind="btnProps"
|
||||||
|
>
|
||||||
|
{{ btnText ? btnText : t('component.cropper.selectImage') }}
|
||||||
|
</a-button>
|
||||||
|
|
||||||
|
<CopperModal
|
||||||
|
@register="register"
|
||||||
|
@upload-success="handleUploadSuccess"
|
||||||
|
:uploadApi="uploadApi"
|
||||||
|
:src="sourceValue"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
computed,
|
||||||
|
CSSProperties,
|
||||||
|
unref,
|
||||||
|
ref,
|
||||||
|
watchEffect,
|
||||||
|
watch,
|
||||||
|
PropType,
|
||||||
|
} from 'vue'
|
||||||
|
import CopperModal from './CopperModal.vue'
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign'
|
||||||
|
import { useModal } from '/@/components/Modal'
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage'
|
||||||
|
import { useI18n } from '/@/hooks/web/useI18n'
|
||||||
|
import type { ButtonProps } from '/@/components/Button'
|
||||||
|
import Icon from '/@/components/Icon'
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
width: { type: [String, Number], default: '200px' },
|
||||||
|
value: { type: String },
|
||||||
|
showBtn: { type: Boolean, default: true },
|
||||||
|
btnProps: { type: Object as PropType<ButtonProps> },
|
||||||
|
btnText: { type: String, default: '' },
|
||||||
|
uploadApi: { type: Function as PropType<({ file: Blob, name: string }) => Promise<void>> },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'CropperAvatar',
|
||||||
|
components: { CopperModal, Icon },
|
||||||
|
props,
|
||||||
|
emits: ['update:value', 'change'],
|
||||||
|
setup(props, { emit, expose }) {
|
||||||
|
const sourceValue = ref(props.value || '')
|
||||||
|
const { prefixCls } = useDesign('cropper-avatar')
|
||||||
|
const [register, { openModal, closeModal }] = useModal()
|
||||||
|
const { createMessage } = useMessage()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const getClass = computed(() => [prefixCls])
|
||||||
|
|
||||||
|
const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px')
|
||||||
|
|
||||||
|
const getIconWidth = computed(() => parseInt(`${props.width}`.replace(/px/, '')) / 2 + 'px')
|
||||||
|
|
||||||
|
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) }))
|
||||||
|
|
||||||
|
const getImageWrapperStyle = computed(
|
||||||
|
(): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }),
|
||||||
|
)
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
sourceValue.value = props.value || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => sourceValue.value,
|
||||||
|
(v: string) => {
|
||||||
|
emit('update:value', v)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function handleUploadSuccess({ source, data }) {
|
||||||
|
sourceValue.value = source
|
||||||
|
emit('change', { source, data })
|
||||||
|
createMessage.success(t('component.cropper.uploadSuccess'))
|
||||||
|
}
|
||||||
|
|
||||||
|
expose({ openModal: openModal.bind(null, true), closeModal })
|
||||||
|
|
||||||
|
return {
|
||||||
|
t,
|
||||||
|
prefixCls,
|
||||||
|
register,
|
||||||
|
openModal: openModal as any,
|
||||||
|
getIconWidth,
|
||||||
|
sourceValue,
|
||||||
|
getClass,
|
||||||
|
getImageWrapperStyle,
|
||||||
|
getStyle,
|
||||||
|
handleUploadSuccess,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@prefix-cls: ~'@{namespace}-cropper-avatar';
|
||||||
|
|
||||||
|
.@{prefix-cls} {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&-image-wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
background: @component-background;
|
||||||
|
border: 1px solid @border-color-base;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-image-mask {
|
||||||
|
opacity: 0%;
|
||||||
|
position: absolute;
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
border-radius: inherit;
|
||||||
|
border: inherit;
|
||||||
|
background: rgb(0 0 0 / 40%);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.4s;
|
||||||
|
|
||||||
|
::v-deep(svg) {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-image-mask:hover {
|
||||||
|
opacity: 4000%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-upload-btn {
|
||||||
|
margin: 10px auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
8
frontend/vben/src/components/Cropper/src/typing.ts
Normal file
8
frontend/vben/src/components/Cropper/src/typing.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type Cropper from 'cropperjs'
|
||||||
|
|
||||||
|
export interface CropendResult {
|
||||||
|
imgBase64: string
|
||||||
|
imgInfo: Cropper.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { Cropper }
|
||||||
@@ -42,7 +42,7 @@ export function useUploadType({
|
|||||||
|
|
||||||
const accept = unref(acceptRef)
|
const accept = unref(acceptRef)
|
||||||
if (accept.length > 0) {
|
if (accept.length > 0) {
|
||||||
helpTexts.push(t('component.upload.accept', [accept.join(',')]))
|
helpTexts.push(t('component.upload.accept', ['docx']))
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxSize = unref(maxSizeRef)
|
const maxSize = unref(maxSizeRef)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export enum PageEnum {
|
|||||||
BASE_HOME = '/dashboard',
|
BASE_HOME = '/dashboard',
|
||||||
// file list path
|
// file list path
|
||||||
PAGE_FILELIST = '/filelist',
|
PAGE_FILELIST = '/filelist',
|
||||||
|
// personal settings path
|
||||||
|
PAGE_SETTINGS = '/settings',
|
||||||
// error page path
|
// error page path
|
||||||
ERROR_PAGE = '/exception',
|
ERROR_PAGE = '/exception',
|
||||||
// error log page path
|
// error log page path
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ export const UserDropDown = createAsyncComponent(() => import('./user-dropdown/i
|
|||||||
|
|
||||||
export const LayoutBreadcrumb = createAsyncComponent(() => import('./Breadcrumb.vue'))
|
export const LayoutBreadcrumb = createAsyncComponent(() => import('./Breadcrumb.vue'))
|
||||||
|
|
||||||
export const Notify = createAsyncComponent(() => import('./notify/index.vue'))
|
// export const Notify = createAsyncComponent(() => import('./notify/index.vue'))
|
||||||
|
|
||||||
export { FullScreen }
|
export { FullScreen }
|
||||||
|
|||||||
@@ -12,12 +12,11 @@
|
|||||||
<template #overlay>
|
<template #overlay>
|
||||||
<Menu @click="handleMenuClick">
|
<Menu @click="handleMenuClick">
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key="doc"
|
key="settings"
|
||||||
:text="t('layout.header.dropdownItemDoc')"
|
:text="t('layout.header.dropdownItemSettings')"
|
||||||
icon="ion:document-text-outline"
|
icon="ion:settings-outline"
|
||||||
v-if="getShowDoc"
|
|
||||||
/>
|
/>
|
||||||
<MenuDivider v-if="getShowDoc" />
|
<MenuDivider />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key="logout"
|
key="logout"
|
||||||
:text="t('layout.header.dropdownItemLoginOut')"
|
:text="t('layout.header.dropdownItemLoginOut')"
|
||||||
@@ -34,21 +33,19 @@
|
|||||||
|
|
||||||
import { defineComponent, computed } from 'vue'
|
import { defineComponent, computed } from 'vue'
|
||||||
|
|
||||||
import { DOC_URL } from '/@/settings/siteSetting'
|
|
||||||
|
|
||||||
import { useUserStore } from '/@/store/modules/user'
|
import { useUserStore } from '/@/store/modules/user'
|
||||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'
|
|
||||||
import { useI18n } from '/@/hooks/web/useI18n'
|
import { useI18n } from '/@/hooks/web/useI18n'
|
||||||
import { useDesign } from '/@/hooks/web/useDesign'
|
import { useDesign } from '/@/hooks/web/useDesign'
|
||||||
import { useModal } from '/@/components/Modal'
|
import { useModal } from '/@/components/Modal'
|
||||||
|
|
||||||
import headerImg from '/@/assets/images/header.jpg'
|
import headerImg from '/@/assets/images/header.jpg'
|
||||||
import { propTypes } from '/@/utils/propTypes'
|
import { propTypes } from '/@/utils/propTypes'
|
||||||
import { openWindow } from '/@/utils'
|
import { router } from '/@/router'
|
||||||
|
import { PageEnum } from '/@/enums/pageEnum'
|
||||||
|
|
||||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'
|
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'
|
||||||
|
|
||||||
type MenuEvent = 'logout' | 'doc'
|
type MenuEvent = 'logout' | 'settings'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'UserDropdown',
|
name: 'UserDropdown',
|
||||||
@@ -64,7 +61,6 @@
|
|||||||
setup() {
|
setup() {
|
||||||
const { prefixCls } = useDesign('header-user-dropdown')
|
const { prefixCls } = useDesign('header-user-dropdown')
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { getShowDoc } = useHeaderSetting()
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const getUserInfo = computed(() => {
|
const getUserInfo = computed(() => {
|
||||||
@@ -79,18 +75,13 @@
|
|||||||
userStore.confirmLoginOut()
|
userStore.confirmLoginOut()
|
||||||
}
|
}
|
||||||
|
|
||||||
// open doc
|
|
||||||
function openDoc() {
|
|
||||||
openWindow(DOC_URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMenuClick(e: MenuInfo) {
|
function handleMenuClick(e: MenuInfo) {
|
||||||
switch (e.key as MenuEvent) {
|
switch (e.key as MenuEvent) {
|
||||||
case 'logout':
|
case 'logout':
|
||||||
handleLoginOut()
|
handleLoginOut()
|
||||||
break
|
break
|
||||||
case 'doc':
|
case 'settings':
|
||||||
openDoc()
|
router.push(PageEnum.PAGE_SETTINGS)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +91,6 @@
|
|||||||
t,
|
t,
|
||||||
getUserInfo,
|
getUserInfo,
|
||||||
handleMenuClick,
|
handleMenuClick,
|
||||||
getShowDoc,
|
|
||||||
register,
|
register,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<div :class="`${prefixCls}-action`">
|
<div :class="`${prefixCls}-action`">
|
||||||
<AppSearch :class="`${prefixCls}-action__item `" v-if="getShowSearch" />
|
<AppSearch :class="`${prefixCls}-action__item `" v-if="getShowSearch" />
|
||||||
|
|
||||||
<Notify v-if="getShowNotice" :class="`${prefixCls}-action__item notify-item`" />
|
<!--<Notify v-if="getShowNotice" :class="`${prefixCls}-action__item notify-item`" />-->
|
||||||
|
|
||||||
<FullScreen v-if="getShowFullScreen" :class="`${prefixCls}-action__item fullscreen-item`" />
|
<FullScreen v-if="getShowFullScreen" :class="`${prefixCls}-action__item fullscreen-item`" />
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
import { SettingButtonPositionEnum } from '/@/enums/appEnum'
|
import { SettingButtonPositionEnum } from '/@/enums/appEnum'
|
||||||
// import { AppLocalePicker } from '/@/components/Application'
|
// import { AppLocalePicker } from '/@/components/Application'
|
||||||
|
|
||||||
import { UserDropDown, LayoutBreadcrumb, FullScreen, Notify } from './components'
|
import { UserDropDown, LayoutBreadcrumb, FullScreen } from './components'
|
||||||
import { useAppInject } from '/@/hooks/web/useAppInject'
|
import { useAppInject } from '/@/hooks/web/useAppInject'
|
||||||
import { useDesign } from '/@/hooks/web/useDesign'
|
import { useDesign } from '/@/hooks/web/useDesign'
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
UserDropDown,
|
UserDropDown,
|
||||||
// AppLocalePicker,
|
// AppLocalePicker,
|
||||||
FullScreen,
|
FullScreen,
|
||||||
Notify,
|
// Notify,
|
||||||
AppSearch,
|
AppSearch,
|
||||||
SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue'), {
|
SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue'), {
|
||||||
loading: true,
|
loading: true,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export default {
|
|||||||
footer: { onlinePreview: '在线预览', onlineDocument: '在线文档' },
|
footer: { onlinePreview: '在线预览', onlineDocument: '在线文档' },
|
||||||
header: {
|
header: {
|
||||||
// user dropdown
|
// user dropdown
|
||||||
dropdownItemDoc: '文档',
|
dropdownItemSettings: '设置',
|
||||||
dropdownItemLoginOut: '退出系统',
|
dropdownItemLoginOut: '退出系统',
|
||||||
|
|
||||||
// tooltip
|
// tooltip
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default {
|
export default {
|
||||||
name: '文件列表',
|
name: '试卷库',
|
||||||
}
|
}
|
||||||
|
|||||||
3
frontend/vben/src/locales/lang/zh-CN/routes/settings.ts
Normal file
3
frontend/vben/src/locales/lang/zh-CN/routes/settings.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
name: '个人设置',
|
||||||
|
}
|
||||||
17
frontend/vben/src/router/menus/modules/filelist.ts
Normal file
17
frontend/vben/src/router/menus/modules/filelist.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { MenuModule } from '/@/router/types'
|
||||||
|
import { t } from '/@/hooks/web/useI18n'
|
||||||
|
const menu: MenuModule = {
|
||||||
|
orderNo: 20,
|
||||||
|
menu: {
|
||||||
|
name: t('routes.filelist.name'),
|
||||||
|
path: '/filelist',
|
||||||
|
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
name: t('routes.filelist.name'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
export default menu
|
||||||
31
frontend/vben/src/router/routes/modules/settings.ts
Normal file
31
frontend/vben/src/router/routes/modules/settings.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { AppRouteModule } from '/@/router/types'
|
||||||
|
|
||||||
|
import { LAYOUT } from '/@/router/constant'
|
||||||
|
import { t } from '/@/hooks/web/useI18n'
|
||||||
|
|
||||||
|
const settings: AppRouteModule = {
|
||||||
|
path: '/settings',
|
||||||
|
name: 'Settings',
|
||||||
|
component: LAYOUT,
|
||||||
|
redirect: '/settings/index',
|
||||||
|
meta: {
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
icon: 'ion:settings-outline',
|
||||||
|
title: t('routes.settings.name'),
|
||||||
|
orderNo: 200,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
name: 'SettingsPage',
|
||||||
|
component: () => import('/@/views/page/settings/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: t('routes.settings.name'),
|
||||||
|
icon: 'ion:settings-outline',
|
||||||
|
hideMenu: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default settings
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card title="最新动态" v-bind="$attrs">
|
<Card title="我的消息" v-bind="$attrs">
|
||||||
<template #extra>
|
|
||||||
<a-button type="link" size="small">更多</a-button>
|
|
||||||
</template>
|
|
||||||
<List item-layout="horizontal" :data-source="dynamicInfoItems">
|
<List item-layout="horizontal" :data-source="dynamicInfoItems">
|
||||||
<template #renderItem="{ item }">
|
<template #renderItem="{ item }">
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -16,6 +13,7 @@
|
|||||||
<Icon :icon="item.avatar" :size="30" />
|
<Icon :icon="item.avatar" :size="30" />
|
||||||
</template>
|
</template>
|
||||||
</ListItemMeta>
|
</ListItemMeta>
|
||||||
|
<a-button ghost color="error">删除</a-button>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</template>
|
</template>
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Card title="项目" v-bind="$attrs">
|
|
||||||
<template #extra>
|
|
||||||
<a-button type="link" size="small" @click="nav2filelist">更多</a-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<CardGrid v-for="item in items" :key="item.title" class="!md:w-1/3 !w-full">
|
|
||||||
<span class="flex">
|
|
||||||
<Icon :icon="item.icon" :color="item.color" size="30" />
|
|
||||||
<span class="text-lg ml-4">{{ item.title }}</span>
|
|
||||||
</span>
|
|
||||||
<div class="flex mt-2 h-10 text-secondary">{{ item.desc }}</div>
|
|
||||||
<div class="flex justify-between text-secondary">
|
|
||||||
<span>{{ item.group }}</span>
|
|
||||||
<span>{{ item.date }}</span>
|
|
||||||
</div>
|
|
||||||
</CardGrid>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue'
|
|
||||||
import { Card } from 'ant-design-vue'
|
|
||||||
import { Icon } from '/@/components/Icon'
|
|
||||||
import { getFileList } from '/@/api/page/page'
|
|
||||||
import { router } from '/@/router'
|
|
||||||
import { PageEnum } from '/@/enums/pageEnum'
|
|
||||||
|
|
||||||
async function nav2filelist() {
|
|
||||||
router.push(PageEnum.PAGE_FILELIST)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fl = await getFileList(6)
|
|
||||||
|
|
||||||
for (let i = 0; i < fl.length; i++) {
|
|
||||||
fl[i].icon = 'ion:newspaper-outline'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { Card, CardGrid: Card.Grid, Icon },
|
|
||||||
setup() {
|
|
||||||
return { items: fl, nav2filelist: nav2filelist }
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Card title="快捷导航" v-bind="$attrs">
|
|
||||||
<CardGrid v-for="item in navItems" :key="item">
|
|
||||||
<span class="flex flex-col items-center">
|
|
||||||
<Icon :icon="item.icon" :color="item.color" size="20" />
|
|
||||||
<span class="text-md mt-2">{{ item.title }}</span>
|
|
||||||
</span>
|
|
||||||
</CardGrid>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Card } from 'ant-design-vue'
|
|
||||||
import { navItems } from './data'
|
|
||||||
import { Icon } from '/@/components/Icon'
|
|
||||||
|
|
||||||
const CardGrid = Card.Grid
|
|
||||||
</script>
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Card title="销售统计" :loading="loading">
|
|
||||||
<div ref="chartRef" :style="{ width, height }"></div>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Ref, ref, watch } from 'vue'
|
|
||||||
import { Card } from 'ant-design-vue'
|
|
||||||
import { useECharts } from '/@/hooks/web/useECharts'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
loading: Boolean,
|
|
||||||
width: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: '100%',
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: '400px',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const chartRef = ref<HTMLDivElement | null>(null)
|
|
||||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
|
|
||||||
watch(
|
|
||||||
() => props.loading,
|
|
||||||
() => {
|
|
||||||
if (props.loading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setOptions({
|
|
||||||
legend: {
|
|
||||||
bottom: 0,
|
|
||||||
data: ['Visits', 'Sales'],
|
|
||||||
},
|
|
||||||
tooltip: {},
|
|
||||||
radar: {
|
|
||||||
radius: '60%',
|
|
||||||
splitNumber: 8,
|
|
||||||
indicator: [
|
|
||||||
{
|
|
||||||
name: '2017',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '2017',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '2018',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '2019',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '2020',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '2021',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'radar',
|
|
||||||
symbolSize: 0,
|
|
||||||
areaStyle: {
|
|
||||||
shadowBlur: 0,
|
|
||||||
shadowColor: 'rgba(0,0,0,.2)',
|
|
||||||
shadowOffsetX: 0,
|
|
||||||
shadowOffsetY: 10,
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
value: [90, 50, 86, 40, 50, 20],
|
|
||||||
name: 'Visits',
|
|
||||||
itemStyle: {
|
|
||||||
color: '#b6a2de',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: [70, 75, 70, 76, 20, 85],
|
|
||||||
name: 'Sales',
|
|
||||||
itemStyle: {
|
|
||||||
color: '#67e0e3',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -2,21 +2,21 @@
|
|||||||
<div class="lg:flex">
|
<div class="lg:flex">
|
||||||
<Avatar :src="userinfo.avatar || headerImg" :size="72" class="!mx-auto !block" />
|
<Avatar :src="userinfo.avatar || headerImg" :size="72" class="!mx-auto !block" />
|
||||||
<div class="md:ml-6 flex flex-col justify-center md:mt-0 mt-2">
|
<div class="md:ml-6 flex flex-col justify-center md:mt-0 mt-2">
|
||||||
<h1 class="md:text-lg text-md">早安, {{ userinfo.realName }}, 开始您一天的工作吧!</h1>
|
<h1 class="md:text-lg text-md">
|
||||||
<span class="text-secondary"> 今日晴,20℃ - 32℃! </span>
|
{{
|
||||||
|
((): string => {
|
||||||
|
let hour: number = new Date().getHours()
|
||||||
|
if (hour < 10) return '早'
|
||||||
|
else if (hour < 19) return '午'
|
||||||
|
else return '晚'
|
||||||
|
})()
|
||||||
|
}}安, {{ userinfo.realName }}, 要注意劳逸结合哦!</h1
|
||||||
|
>
|
||||||
|
<span class="text-secondary"> 今日共有1人上线 </span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 justify-end md:mt-0 mt-4">
|
<div class="flex flex-1 justify-end md:mt-0 mt-4">
|
||||||
<div class="flex flex-col justify-center text-right">
|
|
||||||
<span class="text-secondary"> 待办 </span>
|
|
||||||
<span class="text-2xl">2/10</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col justify-center text-right md:mx-16 mx-12">
|
|
||||||
<span class="text-secondary"> 项目 </span>
|
|
||||||
<span class="text-2xl">8</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-center text-right md:mr-10 mr-4">
|
<div class="flex flex-col justify-center text-right md:mr-10 mr-4">
|
||||||
<span class="text-secondary"> 团队 </span>
|
<span class="text-secondary"> 课程组人数 </span>
|
||||||
<span class="text-2xl">300</span>
|
<span class="text-2xl">300</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,31 +2,17 @@
|
|||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<template #headerContent> <WorkbenchHeader /> </template>
|
<template #headerContent> <WorkbenchHeader /> </template>
|
||||||
<div class="lg:flex">
|
<div class="lg:flex">
|
||||||
<div class="lg:w-7/10 w-full !mr-4 enter-y">
|
<div class="w-full">
|
||||||
<ProjectCard :loading="loading" class="enter-y" />
|
<DynamicInfo :loading="loading" class="enter-y" />
|
||||||
<DynamicInfo :loading="loading" class="!my-4 enter-y" />
|
|
||||||
</div>
|
|
||||||
<div class="lg:w-3/10 w-full enter-y">
|
|
||||||
<QuickNav :loading="loading" class="enter-y" />
|
|
||||||
|
|
||||||
<Card class="!my-4 enter-y" :loading="loading">
|
|
||||||
<img class="xl:h-50 h-30 mx-auto" src="../../../assets/svg/illustration.svg" />
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<SaleRadar :loading="loading" class="enter-y" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Card } from 'ant-design-vue'
|
|
||||||
import { PageWrapper } from '/@/components/Page'
|
import { PageWrapper } from '/@/components/Page'
|
||||||
import WorkbenchHeader from './components/WorkbenchHeader.vue'
|
import WorkbenchHeader from './components/WorkbenchHeader.vue'
|
||||||
import ProjectCard from './components/ProjectCard.vue'
|
|
||||||
import QuickNav from './components/QuickNav.vue'
|
|
||||||
import DynamicInfo from './components/DynamicInfo.vue'
|
import DynamicInfo from './components/DynamicInfo.vue'
|
||||||
import SaleRadar from './components/SaleRadar.vue'
|
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
export const cardList = (() => {
|
export const cardList = (() => {
|
||||||
const result: any[] = []
|
const result: any[] = []
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
result.push({
|
result.push({
|
||||||
id: i,
|
id: i,
|
||||||
title: 'Vben Admin',
|
title: 'Vben Admin',
|
||||||
description: '基于Vue Next, TypeScript, Ant Design Vue实现的一套完整的企业级后台管理系统',
|
description: '基于Vue Next, TypeScript, Ant Design Vue实现的一套完整的企业级后台管理系统',
|
||||||
datetime: '2020-11-26 17:39',
|
datetime: '2020-11-26 17:39',
|
||||||
icon: 'logos:vue',
|
icon: 'bi:filetype-docx',
|
||||||
color: '#1890ff',
|
color: '#1890ff',
|
||||||
author: 'Vben',
|
author: 'Vben',
|
||||||
percent: 20 * (i + 1),
|
percent: i,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<PageWrapper :class="prefixCls" title="试卷资源管理器">
|
<PageWrapper :class="prefixCls" :title="t('routes.filelist.name')">
|
||||||
|
<template #headerContent>
|
||||||
|
<BasicUpload
|
||||||
|
v-if="hasPermission([RoleEnum.SUPER, RoleEnum.FILE_MANAGER])"
|
||||||
|
:maxSize="20"
|
||||||
|
:maxNumber="10"
|
||||||
|
@change="handleChange"
|
||||||
|
:api="uploadApi"
|
||||||
|
:accept="['application/vnd.openxmlformats-officedocument.wordprocessingml.document']"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<div :class="`${prefixCls}__top`">
|
<div :class="`${prefixCls}__top`">
|
||||||
<a-row :gutter="12">
|
<a-row :gutter="12">
|
||||||
<a-col :span="8" :class="`${prefixCls}__top-col`">
|
<a-col :span="8" :class="`${prefixCls}__top-col`">
|
||||||
@@ -19,7 +29,10 @@
|
|||||||
|
|
||||||
<div :class="`${prefixCls}__content`">
|
<div :class="`${prefixCls}__content`">
|
||||||
<a-list :pagination="pagination">
|
<a-list :pagination="pagination">
|
||||||
<template v-for="item in list" :key="item.id">
|
<template
|
||||||
|
v-for="item in getListOfPage(pagination.pageSize)[pagination.current - 1]"
|
||||||
|
:key="item.id"
|
||||||
|
>
|
||||||
<a-list-item class="list">
|
<a-list-item class="list">
|
||||||
<a-list-item-meta>
|
<a-list-item-meta>
|
||||||
<template #avatar>
|
<template #avatar>
|
||||||
@@ -28,11 +41,19 @@
|
|||||||
<template #title>
|
<template #title>
|
||||||
<span>{{ item.title }}</span>
|
<span>{{ item.title }}</span>
|
||||||
<div class="extra">
|
<div class="extra">
|
||||||
<a-button ghost color="success"> 成功 </a-button>
|
<a-button ghost color="success"> 查阅 </a-button>
|
||||||
|
|
||||||
<a-button ghost color="warning"> 警告 </a-button>
|
<a-button
|
||||||
|
ghost
|
||||||
|
color="warning"
|
||||||
|
v-if="hasPermission([RoleEnum.SUPER, RoleEnum.FILE_MANAGER])"
|
||||||
|
>
|
||||||
|
解析
|
||||||
|
</a-button>
|
||||||
|
|
||||||
<a-button ghost color="error"> 错误 </a-button>
|
<a-button ghost color="error" v-if="hasPermission([RoleEnum.SUPER])">
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #description>
|
<template #description>
|
||||||
@@ -40,10 +61,12 @@
|
|||||||
{{ item.description }}
|
{{ item.description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div><span>Owner</span>{{ item.author }}</div>
|
<div><span>文件大小</span>1MB</div>
|
||||||
<div><span>开始时间</span>{{ item.datetime }}</div>
|
<div><span>上传用户</span>{{ item.author }}</div>
|
||||||
|
<div><span>上传时间</span>{{ item.datetime }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
|
<div><span>解析进度</span></div>
|
||||||
<Progress :percent="item.percent" status="active" />
|
<Progress :percent="item.percent" status="active" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -57,12 +80,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Progress, Row, Col } from 'ant-design-vue'
|
import { Progress, Row, Col } from 'ant-design-vue'
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import Icon from '/@/components/Icon/index'
|
import { Icon } from '/@/components/Icon'
|
||||||
|
import { BasicUpload } from '/@/components/Upload'
|
||||||
import { cardList } from './data'
|
import { cardList } from './data'
|
||||||
import { PageWrapper } from '/@/components/Page'
|
import { PageWrapper } from '/@/components/Page'
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage'
|
||||||
|
import { usePermission } from '/@/hooks/web/usePermission'
|
||||||
|
import { RoleEnum } from '/@/enums/roleEnum'
|
||||||
import { List } from 'ant-design-vue'
|
import { List } from 'ant-design-vue'
|
||||||
|
import { uploadApi } from '/@/api/sys/upload'
|
||||||
|
import { useI18n } from '/@/hooks/web/useI18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
BasicUpload,
|
||||||
Icon,
|
Icon,
|
||||||
Progress,
|
Progress,
|
||||||
PageWrapper,
|
PageWrapper,
|
||||||
@@ -73,12 +106,39 @@
|
|||||||
[Col.name]: Col,
|
[Col.name]: Col,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
|
const { createMessage } = useMessage()
|
||||||
|
const { hasPermission } = usePermission()
|
||||||
|
|
||||||
|
function getListOfPage(pageSize: number): any[] {
|
||||||
|
let listOfPage: any[] = []
|
||||||
|
for (let i = 0; i < cardList.length / pageSize; i++) {
|
||||||
|
listOfPage.push(cardList.slice(i * pageSize, (i + 1) * pageSize))
|
||||||
|
}
|
||||||
|
if (cardList.length % pageSize) {
|
||||||
|
listOfPage.push(cardList.slice((cardList.length / pageSize) * pageSize))
|
||||||
|
}
|
||||||
|
return listOfPage
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
t,
|
||||||
|
RoleEnum,
|
||||||
|
handleChange: (list: string[]) => {
|
||||||
|
createMessage.info(`已上传文件${JSON.stringify(list)}`)
|
||||||
|
},
|
||||||
|
uploadApi,
|
||||||
|
hasPermission,
|
||||||
prefixCls: 'list-basic',
|
prefixCls: 'list-basic',
|
||||||
list: cardList,
|
getListOfPage: getListOfPage,
|
||||||
pagination: {
|
pagination: {
|
||||||
|
current: 1,
|
||||||
|
total: cardList.length,
|
||||||
show: true,
|
show: true,
|
||||||
pageSize: 3,
|
pageSize: 10,
|
||||||
|
onChange: function (page: number, pageSize: number) {
|
||||||
|
this.current = page
|
||||||
|
this.pageSize = pageSize
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -131,6 +191,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 40%;
|
width: 40%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
vertical-align: top;
|
||||||
div {
|
div {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
|||||||
97
frontend/vben/src/views/page/settings/BaseSetting.vue
Normal file
97
frontend/vben/src/views/page/settings/BaseSetting.vue
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<CollapseContainer title="基本设置" :canExpan="false">
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="14">
|
||||||
|
<BasicForm @register="register" />
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="10">
|
||||||
|
<div class="change-avatar">
|
||||||
|
<div class="mb-2">头像</div>
|
||||||
|
<CropperAvatar
|
||||||
|
:uploadApi="uploadApi"
|
||||||
|
:value="avatar"
|
||||||
|
btnText="更换头像"
|
||||||
|
:btnProps="{ preIcon: 'ant-design:cloud-upload-outlined' }"
|
||||||
|
@change="updateAvatar"
|
||||||
|
width="150"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<Button type="primary" @click="handleSubmit"> 更新基本信息 </Button>
|
||||||
|
</CollapseContainer>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Button, Row, Col } from 'ant-design-vue'
|
||||||
|
import { computed, defineComponent, onMounted } from 'vue'
|
||||||
|
import { BasicForm, useForm } from '/@/components/Form/index'
|
||||||
|
import { CollapseContainer } from '/@/components/Container'
|
||||||
|
import { CropperAvatar } from '/@/components/Cropper'
|
||||||
|
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage'
|
||||||
|
|
||||||
|
import headerImg from '/@/assets/images/header.jpg'
|
||||||
|
import { accountInfoApi } from '/@/api/demo/account'
|
||||||
|
import { baseSetschemas } from './data'
|
||||||
|
import { useUserStore } from '/@/store/modules/user'
|
||||||
|
import { uploadApi } from '/@/api/sys/upload'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
BasicForm,
|
||||||
|
CollapseContainer,
|
||||||
|
Button,
|
||||||
|
ARow: Row,
|
||||||
|
ACol: Col,
|
||||||
|
CropperAvatar,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const { createMessage } = useMessage()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const [register, { setFieldsValue }] = useForm({
|
||||||
|
labelWidth: 120,
|
||||||
|
schemas: baseSetschemas,
|
||||||
|
showActionButtonGroup: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const data = await accountInfoApi()
|
||||||
|
setFieldsValue(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
const avatar = computed(() => {
|
||||||
|
const { avatar } = userStore.getUserInfo
|
||||||
|
console.log(avatar)
|
||||||
|
return avatar || headerImg
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateAvatar({ src, data }) {
|
||||||
|
const userinfo = userStore.getUserInfo
|
||||||
|
userinfo.avatar = src
|
||||||
|
userStore.setUserInfo(userinfo)
|
||||||
|
console.log('data', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
avatar,
|
||||||
|
register,
|
||||||
|
uploadApi: uploadApi as any,
|
||||||
|
updateAvatar,
|
||||||
|
handleSubmit: () => {
|
||||||
|
createMessage.success('更新成功!')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.change-avatar {
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
47
frontend/vben/src/views/page/settings/SecureSetting.vue
Normal file
47
frontend/vben/src/views/page/settings/SecureSetting.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<CollapseContainer title="安全设置" :canExpan="false">
|
||||||
|
<List>
|
||||||
|
<template v-for="item in list" :key="item.key">
|
||||||
|
<ListItem>
|
||||||
|
<ListItemMeta>
|
||||||
|
<template #title>
|
||||||
|
{{ item.title }}
|
||||||
|
<div class="extra" v-if="item.extra">
|
||||||
|
{{ item.extra }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<div>{{ item.description }}</div>
|
||||||
|
</template>
|
||||||
|
</ListItemMeta>
|
||||||
|
</ListItem>
|
||||||
|
</template>
|
||||||
|
</List>
|
||||||
|
</CollapseContainer>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { List } from 'ant-design-vue'
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { CollapseContainer } from '/@/components/Container/index'
|
||||||
|
|
||||||
|
import { secureSettingList } from './data'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { CollapseContainer, List, ListItem: List.Item, ListItemMeta: List.Item.Meta },
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
list: secureSettingList,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.extra {
|
||||||
|
float: right;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-right: 30px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #1890ff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
68
frontend/vben/src/views/page/settings/data.ts
Normal file
68
frontend/vben/src/views/page/settings/data.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { FormSchema } from '/@/components/Form/index'
|
||||||
|
|
||||||
|
export interface ListItem {
|
||||||
|
key: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
extra?: string
|
||||||
|
avatar?: string
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// tab的list
|
||||||
|
export const settingList = [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
name: '基本设置',
|
||||||
|
component: 'BaseSetting',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
name: '安全设置',
|
||||||
|
component: 'SecureSetting',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 基础设置 form
|
||||||
|
export const baseSetschemas: FormSchema[] = [
|
||||||
|
{
|
||||||
|
field: 'email',
|
||||||
|
component: 'Input',
|
||||||
|
label: '邮箱',
|
||||||
|
colProps: { span: 18 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
component: 'Input',
|
||||||
|
label: '昵称',
|
||||||
|
colProps: { span: 18 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'introduction',
|
||||||
|
component: 'InputTextArea',
|
||||||
|
label: '个人简介',
|
||||||
|
colProps: { span: 18 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'address',
|
||||||
|
component: 'Input',
|
||||||
|
label: '所在地区',
|
||||||
|
colProps: { span: 18 },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 安全设置 list
|
||||||
|
export const secureSettingList: ListItem[] = [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
title: '账户密码',
|
||||||
|
description: '当前密码强度::强',
|
||||||
|
extra: '修改',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
title: '我的手机',
|
||||||
|
description: '已绑定手机::138****8293',
|
||||||
|
extra: '修改',
|
||||||
|
},
|
||||||
|
]
|
||||||
57
frontend/vben/src/views/page/settings/index.vue
Normal file
57
frontend/vben/src/views/page/settings/index.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<ScrollContainer>
|
||||||
|
<div ref="wrapperRef" :class="prefixCls">
|
||||||
|
<Tabs tab-position="left" :tabBarStyle="tabBarStyle">
|
||||||
|
<template v-for="item in settingList" :key="item.key">
|
||||||
|
<TabPane :tab="item.name">
|
||||||
|
<component :is="item.component" />
|
||||||
|
</TabPane>
|
||||||
|
</template>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</ScrollContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { Tabs } from 'ant-design-vue'
|
||||||
|
|
||||||
|
import { ScrollContainer } from '/@/components/Container/index'
|
||||||
|
import { settingList } from './data'
|
||||||
|
|
||||||
|
import BaseSetting from './BaseSetting.vue'
|
||||||
|
import SecureSetting from './SecureSetting.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
ScrollContainer,
|
||||||
|
Tabs,
|
||||||
|
TabPane: Tabs.TabPane,
|
||||||
|
BaseSetting,
|
||||||
|
SecureSetting,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
prefixCls: 'account-setting',
|
||||||
|
settingList,
|
||||||
|
tabBarStyle: {
|
||||||
|
width: '220px',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
.account-setting {
|
||||||
|
margin: 12px;
|
||||||
|
background-color: @component-background;
|
||||||
|
|
||||||
|
.base-title {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab-active {
|
||||||
|
background-color: @item-active-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user