This commit is contained in:
Jason
2023-01-09 19:02:20 +08:00
parent 570198d9bc
commit 5d056280b0
89 changed files with 20634 additions and 1 deletions

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

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

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

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

View 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 }}&nbsp;|&nbsp;
</span>
<span class="mr-5" v-if="showTime">{{ createTime }}</span>
<div v-if="showClick" class="flex items-center">
<ElIcon>
<View />
</ElIcon>
<span>&nbsp;{{ 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>

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

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