mirror of
https://gitee.com/likeadmin/likeadmin_java.git
synced 2026-06-04 08:38:26 +08:00
pc端
This commit is contained in:
73
pc/components/cropper-upload/index.vue
Normal file
73
pc/components/cropper-upload/index.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<div>
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:show-file-list="false"
|
||||
:limit="1"
|
||||
:on-change="handleChange"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<slot />
|
||||
</ElUpload>
|
||||
<ElDialog
|
||||
v-model="state.cropperVisible"
|
||||
:append-to-body="true"
|
||||
:close-on-click-modal="false"
|
||||
:width="600"
|
||||
@close="state.cropperVisible = false"
|
||||
>
|
||||
<div class="h-[400px]">
|
||||
<VueCropper
|
||||
ref="vueCropperRef"
|
||||
:img="state.imagePath"
|
||||
:autoCrop="true"
|
||||
:auto-crop-height="200"
|
||||
:auto-crop-width="200"
|
||||
output-type="png"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="handleConfirmCropper">
|
||||
确认裁剪
|
||||
</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElUpload, ElDialog, ElButton } from 'element-plus'
|
||||
import 'vue-cropper/dist/index.css'
|
||||
import { VueCropper } from 'vue-cropper'
|
||||
import { uploadImage } from '~~/api/app'
|
||||
const emit = defineEmits(['change'])
|
||||
const vueCropperRef = shallowRef()
|
||||
const uploadRef = shallowRef<InstanceType<typeof ElUpload>>()
|
||||
|
||||
const state = reactive({
|
||||
cropperVisible: false,
|
||||
imagePath: ''
|
||||
})
|
||||
|
||||
const handleChange = (rawFile) => {
|
||||
const URL = window.URL || window.webkitURL
|
||||
state.imagePath = URL.createObjectURL(rawFile.raw)
|
||||
state.cropperVisible = true
|
||||
}
|
||||
const handleConfirmCropper = () => {
|
||||
vueCropperRef.value?.getCropBlob(async (file) => {
|
||||
const fileName = `file.${file.type.split('/')[1]}`
|
||||
const imgFile = new window.File([file], fileName, {
|
||||
type: file.type
|
||||
})
|
||||
const data = await uploadImage({ file: imgFile })
|
||||
state.cropperVisible = false
|
||||
emit('change', data.uri)
|
||||
uploadRef.value?.clearFiles()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
28
pc/components/icon/index.vue
Normal file
28
pc/components/icon/index.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<ElIcon v-bind="props" v-if="name.includes(EL_ICON_PREFIX)">
|
||||
<component :is="name" />
|
||||
</ElIcon>
|
||||
<span v-if="name.includes(LOCAL_ICON_PREFIX)" class="local-icon">
|
||||
<SvgIcon v-bind="props" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElIcon } from 'element-plus'
|
||||
import { EL_ICON_PREFIX, LOCAL_ICON_PREFIX } from '~~/plugins/icons'
|
||||
import SvgIcon from './svg-icon.vue'
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '14px'
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'inherit'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
38
pc/components/icon/svg-icon.vue
Normal file
38
pc/components/icon/svg-icon.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<svg aria-hidden="true" :style="styles">
|
||||
<use :xlink:href="symbolId" fill="currentColor" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { addUnit } from '@/utils/util'
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 16
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'inherit'
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const symbolId = computed(() => `#${props.name}`)
|
||||
const styles = computed<CSSProperties>(() => {
|
||||
return {
|
||||
width: addUnit(props.size),
|
||||
height: addUnit(props.size),
|
||||
color: props.color
|
||||
}
|
||||
})
|
||||
return { symbolId, styles }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
115
pc/components/information/card.vue
Normal file
115
pc/components/information/card.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="bg-white rounded-[8px]">
|
||||
<div class="flex items-center h-[60px] border-b border-br ml-5 pr-5">
|
||||
<div class="flex-1 flex min-w-0 mr-4 h-full">
|
||||
<span
|
||||
class="text-2xl truncate font-medium h-full border-b-2 border-tx-primary mt-[1px] flex items-center"
|
||||
>
|
||||
{{ header }}
|
||||
</span>
|
||||
</div>
|
||||
<ElButton class="button" link v-if="link">
|
||||
<NuxtLink :to="link" class="flex">
|
||||
更多
|
||||
<ElIcon><ArrowRight /></ElIcon>
|
||||
</NuxtLink>
|
||||
</ElButton>
|
||||
</div>
|
||||
<slot name="content" :data="data" v-if="data.length">
|
||||
<div class="px-5 pb-5">
|
||||
<template v-for="(item, index) in data" :key="item.id">
|
||||
<slot name="item" :item="item" :index="index">
|
||||
<InformationItems
|
||||
:index="index"
|
||||
:show-sort="showSort"
|
||||
:id="item.id"
|
||||
:title="item.title"
|
||||
:desc="item.intro"
|
||||
:click="item.visit"
|
||||
:author="item.author"
|
||||
:create-time="item.createTime"
|
||||
:image="item.image"
|
||||
:only-title="onlyTitle"
|
||||
:image-size="imageSize"
|
||||
:show-author="showAuthor"
|
||||
:show-desc="showDesc"
|
||||
:show-click="showClick"
|
||||
:border="border"
|
||||
:title-line="titleLine"
|
||||
:show-time="showTime"
|
||||
:source="source"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
</div>
|
||||
</slot>
|
||||
<div v-else>
|
||||
<el-empty
|
||||
:image="empty_news"
|
||||
description="暂无资讯"
|
||||
:image-size="250"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElButton, ElIcon, ElEmpty } from 'element-plus'
|
||||
import empty_news from '@/assets/images/empty_news.png'
|
||||
import { ArrowRight } from '@element-plus/icons-vue'
|
||||
import { PropType } from 'vue'
|
||||
defineProps({
|
||||
header: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
data: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
onlyTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
titleLine: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
imageSize: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
showAuthor: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showDesc: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showClick: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showTime: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showSort: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
168
pc/components/information/items.vue
Normal file
168
pc/components/information/items.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<NuxtLink :to="`/information/detail/${id}`">
|
||||
<div
|
||||
v-if="onlyTitle"
|
||||
class="before:w-[6px] mt-4 before:h-[6px] before:bg-primary before:block flex items-center before:rounded-[6px] before:mr-2.5 before:flex-none"
|
||||
>
|
||||
<slot name="title" :title="title">
|
||||
<span class="line-clamp-1 flex-1 font-medium">{{ title }}</span>
|
||||
</slot>
|
||||
<span class="text-tx-secondary ml-4" v-if="showTime">
|
||||
{{ createTime }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="{
|
||||
'border-b border-br pb-4': border,
|
||||
'flex pt-4 items-center': !isHorizontal
|
||||
}"
|
||||
>
|
||||
<div class="flex relative">
|
||||
<ElImage
|
||||
v-if="image"
|
||||
class="flex-none"
|
||||
:class="{
|
||||
'mr-4': !isHorizontal
|
||||
}"
|
||||
:src="image"
|
||||
fit="cover"
|
||||
:style="getImageStyle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-1"
|
||||
:class="{
|
||||
'p-2': isHorizontal
|
||||
}"
|
||||
>
|
||||
<slot name="title" :title="title">
|
||||
<div
|
||||
class="text-lg font-medium"
|
||||
:class="`line-clamp-${titleLine}`"
|
||||
>
|
||||
{{ title }}
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<div
|
||||
v-if="showDesc && desc"
|
||||
class="text-tx-regular line-clamp-2 mt-4"
|
||||
>
|
||||
{{ desc }}
|
||||
</div>
|
||||
<div
|
||||
v-if="showAuthor || showTime || showClick"
|
||||
class="mt-5 text-tx-secondary flex items-center flex-wrap"
|
||||
>
|
||||
<span v-if="showAuthor && author">
|
||||
{{ author }} |
|
||||
</span>
|
||||
<span class="mr-5" v-if="showTime">{{ createTime }}</span>
|
||||
<div v-if="showClick" class="flex items-center">
|
||||
<ElIcon>
|
||||
<View />
|
||||
</ElIcon>
|
||||
<span> {{ click }}人浏览</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElImage, ElIcon } from 'element-plus'
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
const props = defineProps({
|
||||
index: {
|
||||
type: Number
|
||||
},
|
||||
id: {
|
||||
type: Number
|
||||
},
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
desc: {
|
||||
type: String
|
||||
},
|
||||
image: {
|
||||
type: String
|
||||
},
|
||||
author: {
|
||||
type: String
|
||||
},
|
||||
click: {
|
||||
type: Number
|
||||
},
|
||||
createTime: {
|
||||
type: String
|
||||
},
|
||||
onlyTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isHorizontal: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
titleLine: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
imageSize: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
showAuthor: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showDesc: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showClick: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showTime: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showSort: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const getImageStyle = computed(() => {
|
||||
switch (props.imageSize) {
|
||||
case 'default':
|
||||
return {
|
||||
width: '180px',
|
||||
height: '135px'
|
||||
}
|
||||
case 'mini':
|
||||
return {
|
||||
width: '120px',
|
||||
height: '90px'
|
||||
}
|
||||
case 'large':
|
||||
return {
|
||||
width: '260px',
|
||||
height: '195px'
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
137
pc/components/popover-input/index.vue
Normal file
137
pc/components/popover-input/index.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div @mouseenter="inPopover = true" @mouseleave="inPopover = false">
|
||||
<el-popover
|
||||
placement="top"
|
||||
v-model:visible="visible"
|
||||
:width="width"
|
||||
trigger="contextmenu"
|
||||
class="popover-input"
|
||||
:teleported="teleported"
|
||||
:persistent="false"
|
||||
popper-class="!p-0"
|
||||
>
|
||||
<div class="flex p-3" @click.stop="">
|
||||
<div class="popover-input__input mr-[10px] flex-1">
|
||||
<el-select
|
||||
class="flex-1"
|
||||
:size="size"
|
||||
v-if="type == 'select'"
|
||||
v-model="inputValue"
|
||||
:teleported="teleported"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-else
|
||||
v-model.trim="inputValue"
|
||||
:maxlength="limit"
|
||||
:show-word-limit="showLimit"
|
||||
:type="type"
|
||||
:size="size"
|
||||
clearable
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
</div>
|
||||
<div class="popover-input__btns flex-none">
|
||||
<el-button link @click="close">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:size="size"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<template #reference>
|
||||
<div class="inline" @click.stop="handleOpen">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { ElPopover, ElButton, ElSelect, ElOption, ElInput } from 'element-plus'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: '300px'
|
||||
},
|
||||
placeholder: String,
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<'default' | 'small' | 'large'>,
|
||||
default: 'default'
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
showLimit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
teleported: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['confirm'])
|
||||
const visible = ref(false)
|
||||
const inPopover = ref(false)
|
||||
const inputValue = ref()
|
||||
const handleConfirm = () => {
|
||||
close()
|
||||
emit('confirm', inputValue.value)
|
||||
}
|
||||
const handleOpen = () => {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(value) => {
|
||||
inputValue.value = value
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
useEventListener(document.documentElement, 'click', () => {
|
||||
if (inPopover.value) return
|
||||
close()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
79
pc/components/verification-code/index.vue
Normal file
79
pc/components/verification-code/index.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<ElButton v-if="!isStart" @click="handlStart" link>
|
||||
{{ isRetry ? endText : startText }}
|
||||
</ElButton>
|
||||
<VueCountdown
|
||||
v-else
|
||||
ref="vueCountdownRef"
|
||||
:time="seconds * 1000"
|
||||
v-slot="{ totalSeconds }"
|
||||
@end="handleEnd"
|
||||
>
|
||||
{{ getChangeText(totalSeconds) }}
|
||||
</VueCountdown>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import VueCountdown from '@chenfengyuan/vue-countdown'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import { ElButton } from 'element-plus'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
VueCountdown,
|
||||
ElButton
|
||||
},
|
||||
props: {
|
||||
// 倒计时总秒数
|
||||
seconds: {
|
||||
type: Number,
|
||||
default: 60
|
||||
},
|
||||
// 尚未开始时提示
|
||||
startText: {
|
||||
type: String,
|
||||
default: '获取验证码'
|
||||
},
|
||||
// 正在倒计时中的提示
|
||||
changeText: {
|
||||
type: String,
|
||||
default: 'x秒重新获取'
|
||||
},
|
||||
// 倒计时结束时的提示
|
||||
endText: {
|
||||
type: String,
|
||||
default: '重新获取'
|
||||
}
|
||||
},
|
||||
emits: ['click-get'],
|
||||
setup(props, { emit }) {
|
||||
const isStart = ref(false)
|
||||
const isRetry = ref(false)
|
||||
const start = async () => {
|
||||
isStart.value = true
|
||||
}
|
||||
|
||||
const getChangeText = (second) => {
|
||||
return props.changeText.replace('x', second)
|
||||
}
|
||||
const handleEnd = () => {
|
||||
isStart.value = false
|
||||
isRetry.value = true
|
||||
}
|
||||
const handlStart = useThrottleFn(
|
||||
() => {
|
||||
emit('click-get')
|
||||
},
|
||||
1000,
|
||||
false
|
||||
)
|
||||
return {
|
||||
getChangeText,
|
||||
isStart,
|
||||
start,
|
||||
isRetry,
|
||||
handleEnd,
|
||||
handlStart
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
Reference in New Issue
Block a user