Commit 14475b0a by lijiabin

【需求 17679】 wip: 继续完善首页部分

parent b3cb242d
import { ArticleTypeEnum, ReleaseStatusEnum, BooleanFlag } from '@/constants'
import { ArticleTypeEnum, ReleaseStatusTypeEnum, BooleanFlag } from '@/constants'
import type { PageSearchParams } from '@/utils/request/types'
/**
......@@ -34,7 +34,7 @@ export interface AddOrUpdateArticleDto {
mainTagId?: number
/** 发布状态 */
releaseStatus?: ReleaseStatusEnum
releaseStatus?: ReleaseStatusTypeEnum
/** 标签列表 */
tagList?: { tagId: number; sort: number }[]
......@@ -67,7 +67,7 @@ export interface ArticleItemDto {
isRecommend: BooleanFlag
type: ArticleTypeEnum
isRelateColleague: BooleanFlag
releaseStatus: ReleaseStatusEnum
releaseStatus: ReleaseStatusTypeEnum
tagNameList: string[]
praiseCount: number
collectionCount: number
......
......@@ -10,7 +10,10 @@ export * from './article'
export * from './user'
export * from './case'
export * from './home'
export * from './practice'
export * from './common'
export * from './article'
// 导出类型
export * from './task/types'
export * from './shop/types'
......@@ -22,4 +25,7 @@ export * from './article/types'
export * from './user/types'
export * from './case/types'
export * from './home/types'
export * from './practice/types'
export * from './common/types'
export * from './article/types'
import service from '@/utils/request/index'
import type { AddOrUpdatePracticeDto, ArticleItemDto, ArticleSearchParams } from './types'
import type { BackendServicePageResult } from '@/utils/request/types'
import { ArticleTypeEnum } from '@/constants'
// 关于实践相关接口
/**
* 发布或者更新实践
*/
export const addOrUpdatePractice = (data: AddOrUpdatePracticeDto) => {
return service.request<boolean>({
url: '/api/cultureArticle/addOrUpdatePractice',
method: 'POST',
data,
})
}
import { ArticleTypeEnum, ReleaseStatusTypeEnum, BooleanFlag, SendTypeEnum } from '@/constants'
import type { PageSearchParams } from '@/utils/request/types'
/**
* 添加或更新实践DTO
*/
export interface AddOrUpdatePracticeDto {
id?: number
title: string
content: string
faceUrl: string
imgUrl: string
releaseStatus: ReleaseStatusTypeEnum
mainTagId: number | string
tagList: { tagId: number; sort: number }[]
sendType: SendTypeEnum
sendTime: string
}
/**
* 搜索文章的参数
*/
export interface ArticleSearchParams extends PageSearchParams {
type?: ArticleTypeEnum
}
/**
* 添加或更新文章DTO(带枚举版本)
*/
export interface AddOrUpdateArticleDto {
/** 内容 */
content?: string
/** 创建人id */
createUserId?: number
/** 描述 */
description?: string
/** 封面图 */
faceUrl?: string
/** id,编辑时必传 */
id?: number
/** 是否关联同事 */
isRelateColleague?: number
/** 主标签id */
mainTagId?: number
/** 发布状态 */
releaseStatus?: ReleaseStatusTypeEnum
/** 标签列表 */
tagList?: { tagId: number; sort: number }[]
/** 标题 */
title?: string
/** 文章类型 */
type?: ArticleTypeEnum
/** 视频url */
videoUrl?: string
}
/**
* 文章详情
*/
// 更严格的类型定义
export interface ArticleItemDto {
id: number
title: string
content: string
faceUrl: string
videoUrl: string
description: string
createUserId: number
createTime: number
viewCount: number
isRecommend: BooleanFlag
type: ArticleTypeEnum
isRelateColleague: BooleanFlag
releaseStatus: ReleaseStatusTypeEnum
tagNameList: string[]
praiseCount: number
collectionCount: number
replyCount: number
hasPraised: BooleanFlag
}
import type { SetupContext } from 'vue'
type TypeOfValue = string | number
type TabsProps = {
interface TabsProps<T> {
tabs: {
label: string
value: TypeOfValue
value: T
}[]
modelValue: TypeOfValue
modelValue: T
}
type TabsEmits = {
'update:modelValue': [TypeOfValue]
change: [TypeOfValue]
interface TabsEmits<T> {
'update:modelValue': [T]
change: [T]
}
const BASE_TAB_CALASSES =
......@@ -21,7 +21,10 @@ const ACTIVE_TAB_CLASSES =
' !bg-gradient-to-r !from-[#3b82f6]/90 !to-[#60a5fa]/90 !shadow-lg transform -translate-y-1 !text-white !border-transparent'
// <div class="left flex gap-3"> 未设置排列方式 需要给父组件设置 flex布局
export default function Tabs({ tabs, modelValue }: TabsProps, { emit }: SetupContext<TabsEmits>) {
export default function Tabs<T extends TypeOfValue>(
{ tabs, modelValue }: TabsProps<T>,
{ emit }: SetupContext<TabsEmits<T>>,
) {
return tabs.map((tab) => (
<div
key={tab.value}
......
......@@ -10,6 +10,8 @@
:on-change="handleChange"
:before-remove="handleBeforeRemove"
:multiple="multiple"
:limit="limit"
:disabled="hasReachedLimit && !multiple"
>
<el-icon><Plus /></el-icon>
</el-upload>
......@@ -25,98 +27,164 @@ import { uploadFile as uploadFileApi } from '@/api'
import type { UploadProps, UploadUserFile } from 'element-plus'
import type { UploadFileProps } from './types'
const { limit = 2, multiple = true } = defineProps<UploadFileProps>()
const props = withDefaults(defineProps<UploadFileProps>(), {
limit: 2,
multiple: true,
})
// 用户双向绑定的图片值
const modelValue = defineModel<T>({
required: true,
})
const fileList = ref<UploadUserFile[]>([])
const uploadRef = useTemplateRef('uploadRef')
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
// 传的是否是数组
const isArrayOfModelValue = Array.isArray(modelValue.value)
const isArrayType = computed(() => Array.isArray(modelValue.value))
const hasReachedLimit = computed(() => fileList.value.length >= props.limit)
const isInternalUpdate = ref(false)
// 初始化回显fileList
const initFileList = () => {
let urlArray: string[] = []
if (isArrayOfModelValue) {
urlArray = modelValue.value as string[]
} else {
urlArray = (modelValue.value as string).split(',').filter(Boolean)
}
fileList.value = urlArray.map((url, index) => {
const name = url.split('/').pop()?.split('.').shift() as string
return {
url,
name,
uid: index,
}
const parseModelValueToUrls = (value: T): string[] => {
if (!value) return []
return Array.isArray(value) ? value.filter(Boolean) : (value as string).split(',').filter(Boolean)
}
const formatUrlsToModelValue = (urls: string[]): T => {
return (isArrayType.value ? urls : urls.join(',')) as T
}
const syncFileListToModel = () => {
if (isInternalUpdate.value) return
const urls = fileList.value
.filter((file) => file.status === 'success' && file.url)
.map((file) => file.url!)
isInternalUpdate.value = true
modelValue.value = formatUrlsToModelValue(urls)
nextTick(() => {
isInternalUpdate.value = false
})
}
// 监听 modelValue 变化,重新初始化 fileList
const syncModelToFileList = () => {
const urls = parseModelValueToUrls(modelValue.value)
fileList.value = urls.map((url, index) => ({
uid: Date.now() + index,
name: url.split('/').pop() || `file-${index}`,
url,
status: 'success' as const,
}))
}
watch(
() => modelValue.value,
(newVal) => {
if (newVal != undefined) {
initFileList()
if (isInternalUpdate.value) return
if (newVal !== undefined && newVal !== null) {
syncModelToFileList()
}
},
{ immediate: true },
)
watch(fileList, (newVal) => {
if (isArrayOfModelValue) {
modelValue.value = newVal.map((item) => item.url!) as T
} else {
modelValue.value = newVal.map((item) => item.url!).join(',') as T
watch(
fileList,
() => {
syncFileListToModel()
},
{ deep: true },
)
/**
* 处理文件变化(上传)- 修复版本
*/
const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) => {
// 检查是否超出限制
if (uploadFiles.length > props.limit) {
ElMessage.error(`最多上传 ${props.limit} 个文件`)
const index = fileList.value.findIndex((file) => file.uid === uploadFile.uid)
if (index !== -1) {
fileList.value.splice(index, 1)
}
return
}
})
// 是否达到上传限制了
const hasReachedLimit = computed(() => {
return fileList.value.length >= limit
})
// 如果是新上传的文件
if (uploadFile.raw && uploadFile.status === 'ready') {
// 保存 uid 用于后续查找
const uid = uploadFile.uid
const uploadRef = useTemplateRef('uploadRef')
try {
// 更新状态为上传中(第一次查找)
let fileIndex = fileList.value.findIndex((file) => file.uid === uid)
if (fileIndex !== -1) {
fileList.value[fileIndex].status = 'uploading'
}
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
// 上传文件
const { data } = await uploadFileApi(uploadFile.raw)
// ✅ 上传完成后重新查找索引(第二次查找)
fileIndex = fileList.value.findIndex((file) => file.uid === uid)
if (fileIndex !== -1) {
// 更新文件信息
fileList.value[fileIndex] = {
...fileList.value[fileIndex],
url: data.fileUrl,
name: data.fileName,
status: 'success',
}
ElMessage.success('上传成功')
} else {
console.warn('找不到对应的文件,uid:', uid)
}
} catch (error) {
console.error('上传失败:', error)
ElMessage.error('上传失败,请重试')
// 移除上传失败的文件
const fileIndex = fileList.value.findIndex((file) => file.uid === uid)
if (fileIndex !== -1) {
fileList.value.splice(fileIndex, 1)
}
}
}
}
const handleBeforeRemove: UploadProps['beforeRemove'] = (uploadFile) => {
return ElMessageBox.confirm('确定要删除这个文件吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => true)
.catch(() => false)
}
const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
console.log(uploadFile, uploadFiles)
console.log(uploadFile.url, 'onRemove')
const handleRemove: UploadProps['onRemove'] = (uploadFile) => {
console.log('文件已删除:', uploadFile.name)
}
const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
dialogImageUrl.value = uploadFile.url!
dialogVisible.value = true
console.log(uploadFile, 'onPreview')
}
// 在这里处理上传逻辑
const handleChange = async (uploadFile: UploadUserFile, uploadFiles: UploadUserFile[]) => {
if (hasReachedLimit.value) {
ElMessage.error(`最多上传${limit}个文件`)
uploadFiles.pop()
return false
}
console.log(uploadFile, uploadFiles, 'onChange')
const { data } = await uploadFileApi(uploadFile.raw as File)
const index = fileList.value.findIndex((file) => file.uid === uploadFile.uid)
if (index !== -1) {
fileList.value[index] = {
...fileList.value[index],
url: data.fileUrl,
name: data.fileName,
status: 'success',
}
}
const clearFiles = () => {
fileList.value = []
}
const handleBeforeRemove = (uploadFile: UploadUserFile, uploadFiles: UploadUserFile[]) => {
return true
const submit = () => {
uploadRef.value?.submit()
}
defineExpose({
clearFiles,
submit,
})
</script>
<style scoped></style>
......@@ -9,7 +9,7 @@ export enum ArticleTypeEnum {
}
// 发布状态枚举
export enum ReleaseStatusEnum {
export enum ReleaseStatusTypeEnum {
DRAFT = 1, // 草稿
PUBLISH = 2, // 发布
}
......
export * from './useResetData'
export * from './usePageSearch'
export * from './useScrollTop'
export * from './useHintAnimation'
interface UseHintAnimationOptions {
classes?: string[] // 一次加多个过度类 shortcuts
duration?: number // 动画时长
}
// 用js模拟dom元素的 hover 效果
export const useHintAnimation = (
el: MaybeRef<HTMLElement | null>,
{ classes = [], duration = 200 }: UseHintAnimationOptions = {},
) => {
let timer: number | null = null
const triggerAnimation = () => {
const dom = unref(el)
if (!dom) return
// 清除旧动画,以防连点失效
if (timer) {
classes.forEach((cls) => dom.classList.remove(cls))
clearTimeout(timer)
}
// 添加动画类
classes.forEach((cls) => dom.classList.add(cls))
// 动画结束后移除
timer = setTimeout(() => {
classes.forEach((cls) => dom.classList.remove(cls))
timer = null
}, duration)
}
return { triggerAnimation }
}
import { ArticleTypeEnum, ReleaseStatusEnum } from '@/constants'
import { ArticleTypeEnum, ReleaseStatusTypeEnum } from '@/constants'
import UploadFile from '@/components/common/UploadFile/index.vue'
import { useResetData } from '@/hooks'
export default defineComponent((_, { expose }) => {
export default defineComponent(
(_, { expose }) => {
const [form, resetForm] = useResetData({
title: '',
content: '',
faceUrl: '',
releaseStatus: ReleaseStatusEnum.PUBLISH,
releaseStatus: ReleaseStatusTypeEnum.PUBLISH,
type: ArticleTypeEnum.POST,
})
const formRef = ref<InstanceType<typeof ElForm>>()
......@@ -72,10 +73,10 @@ export default defineComponent((_, { expose }) => {
</el-form-item>
<el-form-item label="发布时间" prop="releaseStatus">
<el-radio-group v-model={form.value.releaseStatus} class="radio-group">
<el-radio value={ReleaseStatusEnum.PUBLISH} class="radio-item immediate">
<el-radio value={ReleaseStatusTypeEnum.PUBLISH} class="radio-item immediate">
立即发布
</el-radio>
<el-radio value={ReleaseStatusEnum.DRAFT} class="radio-item scheduled">
<el-radio value={ReleaseStatusTypeEnum.DRAFT} class="radio-item scheduled">
定时发布
</el-radio>
</el-radio-group>
......@@ -83,4 +84,8 @@ export default defineComponent((_, { expose }) => {
</el-form>
</div>
)
})
},
{
name: 'PostForm',
},
)
import { ArticleTypeEnum, ReleaseStatusEnum, SendTypeEnum } from '@/constants'
import { ReleaseStatusTypeEnum, SendTypeEnum } from '@/constants'
import UploadFile from '@/components/common/UploadFile/index.vue'
import SelectTags from '@/components/common/SelectTags/index.vue'
import { useResetData } from '@/hooks'
import type { TagItemDto } from '@/api/tag/types'
import type { AddOrUpdatePracticeDto } from '@/api/practice/types'
export default defineComponent((_, { expose }) => {
const [form, resetForm] = useResetData({
const [form, resetForm] = useResetData<AddOrUpdatePracticeDto>({
title: '',
content: '',
faceUrl: '',
releaseStatus: ReleaseStatusEnum.PUBLISH,
type: ArticleTypeEnum.PRACTICE,
imgUrl: '',
releaseStatus: ReleaseStatusTypeEnum.PUBLISH,
mainTagId: '',
tagList: [],
sendType: SendTypeEnum.IMMEDIATE,
......@@ -20,18 +21,20 @@ export default defineComponent((_, { expose }) => {
})
const formRef = ref<InstanceType<typeof ElForm>>()
const rules = {
title: [{ required: true, message: '请输入帖子标题', trigger: 'blur' }],
content: [{ required: true, message: '请输入帖子内容', trigger: 'blur' }],
faceUrl: [{ required: true, message: '请上传贴图', trigger: 'change' }],
title: [{ required: true, message: '请输入实践标题', trigger: 'blur' }],
content: [{ required: true, message: '请输入实践内容', trigger: 'blur' }],
imgUrl: [{ required: true, message: '请上传实践图片', trigger: 'change' }],
releaseStatus: [{ required: true, message: '请选择发布时间', trigger: 'blur' }],
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [{ required: true, message: '请选择发布时间', trigger: 'blur' }],
}
const transformForm = () => {
const transformForm = (releaseStatus: ReleaseStatusTypeEnum) => {
return {
...form.value,
releaseStatus,
faceUrl: form.value.imgUrl.split(',')[0],
tagList: [form.value.mainTagId, ...form.value.tagList].map((tag, index) => {
return {
sort: index,
......@@ -41,11 +44,11 @@ export default defineComponent((_, { expose }) => {
}
}
const validate = async () => {
// 检验并且获取表单数据
const getValidatedFormData = async (releaseStatus: ReleaseStatusTypeEnum) => {
try {
await formRef.value?.validate()
console.log(transformForm())
return transformForm()
return transformForm(releaseStatus)
} catch (error) {
console.log(error)
ElMessage.warning('请检查输入内容')
......@@ -64,7 +67,7 @@ export default defineComponent((_, { expose }) => {
}
expose({
validate,
getValidatedFormData,
resetFields,
})
return () => (
......@@ -95,9 +98,9 @@ export default defineComponent((_, { expose }) => {
class="content-input"
/>
</el-form-item>
<el-form-item label="图片" prop="faceUrl">
<el-form-item label="图片" prop="imgUrl">
{/* @ts-ignore */}
<UploadFile v-model={form.value.faceUrl} />
<UploadFile v-model={form.value.imgUrl} />
</el-form-item>
<el-form-item label="主标签" prop="mainTagId">
{{
......
......@@ -9,18 +9,20 @@
align-center
@closed="handleClosed"
>
<div class="bg-white/95 rounded-16px p-24px backdrop-blur-10px">
<div v-loading="loading" class="bg-white/95 rounded-16px p-24px backdrop-blur-10px">
<!-- <keep-alive> -->
<component :is="currentFormComp" ref="formComponentRef" />
<!-- </keep-alive> -->
<!-- 底部按钮 -->
<div class="flex justify-end gap-1">
<el-button @click="handleClosed">取消</el-button>
<el-button @click="handleSaveDraft" class="">存草稿</el-button>
<el-button class="rounded-lg" @click="handleClosed">取消</el-button>
<el-button class="rounded-lg" @click="handleSubmit(ReleaseStatusTypeEnum.DRAFT)">
存草稿
</el-button>
<el-button
type="primary"
@click="handleSubmit"
@click="handleSubmit(ReleaseStatusTypeEnum.PUBLISH)"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
>
发布
......@@ -36,12 +38,15 @@ import type { Component } from 'vue'
// 如果 你已经按需引入了 那么写 这个就会有css bug 所以 这里不写 就是你如果写这个的话 那么 他不会引入css
// import { ElDialog } from 'element-plus'
// import { Plus } from '@element-plus/icons-vue'
import { addOrUpdateArticle } from '@/api'
import { ArticleTypeEnum } from '@/constants'
import { addOrUpdateArticle, addOrUpdatePractice } from '@/api'
import { ArticleTypeEnum, ReleaseStatusTypeEnum } from '@/constants'
import PostForm from './postForm.tsx'
import PracticeForm from './practiceForm.tsx'
const typeMap: Record<ArticleTypeEnum, { title: string; component: Component }> = {
const typeMap: Record<
ArticleTypeEnum,
{ title: string; component: Component; api?: (data: any) => Promise<any> }
> = {
[ArticleTypeEnum.VIDEO]: {
title: '视频',
component: PostForm,
......@@ -57,6 +62,7 @@ const typeMap: Record<ArticleTypeEnum, { title: string; component: Component }>
[ArticleTypeEnum.PRACTICE]: {
title: '实践',
component: defineAsyncComponent(() => import('./practiceForm.tsx')),
api: addOrUpdatePractice,
},
[ArticleTypeEnum.COLUMN]: {
title: '专栏',
......@@ -80,6 +86,7 @@ const currentFormComp = computed(() => {
const dialogVisible = ref(false)
const articleType = ref<ArticleTypeEnum>(ArticleTypeEnum.PRACTICE)
const loading = ref(false)
// 打开弹窗
const open = (type: ArticleTypeEnum) => {
......@@ -98,18 +105,23 @@ const handleClosed = () => {
formComponentRef.value?.resetFields()
}
const handleSaveDraft = async () => {}
const handleSubmit = async () => {
const formData = await formComponentRef.value?.validate()
const handleSubmit = async (releaseStatus: ReleaseStatusTypeEnum) => {
loading.value = true
try {
const formData = await formComponentRef.value?.getValidatedFormData(releaseStatus)
if (!formData) return
console.log(formData)
await addOrUpdateArticle({
await typeMap[articleType.value].api?.({
...formData,
})
ElMessage.success('发布成功')
// 这里可以添加发布逻辑
close()
} catch (error) {
console.log(error)
} finally {
loading.value = false
}
}
// 暴露方法给父组件
......
......@@ -114,12 +114,6 @@
</div>
</div>
</div>
<!-- <template v-else-if="loading">
<div class="flex items-center justify-center h-full">
<el-icon class="is-loading mr-2 text-gray-500"><Loading /></el-icon>
<span class="text-gray-500">加载中...</span>
</div>
</template> -->
<template v-else>
<div class="flex items-center justify-center h-full">
<el-empty description="暂无数据" />
......@@ -137,7 +131,7 @@ import dayjs from 'dayjs'
const router = useRouter()
const { list, total, searchParams, loading, goToPage, changePageSize } = usePageSearch(
const { list, total, searchParams, loading, goToPage, changePageSize, reset } = usePageSearch(
getArticleList,
{
defaultParams: { type: ArticleTypeEnum.POST },
......@@ -148,90 +142,8 @@ const { list, total, searchParams, loading, goToPage, changePageSize } = usePage
const tabsRef = inject(TABS_REF_KEY)
const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
</script>
<!-- <style scoped>
/* 自定义分页器样式 */
:deep(.custom-pagination) {
--el-pagination-font-size: 14px;
--el-pagination-bg-color: transparent;
--el-pagination-text-color: #6b7280;
--el-pagination-border-radius: 8px;
--el-pagination-button-color: #6b7280;
--el-pagination-button-bg-color: #f9fafb;
--el-pagination-hover-color: #3b82f6;
--el-pagination-hover-bg-color: #eff6ff;
}
/* 分页按钮样式 */
:deep(.custom-pagination .el-pagination__btn) {
border: none;
border-radius: 8px;
transition: all 0.2s;
}
:deep(.custom-pagination .el-pagination__btn:hover) {
transform: scale(1.05);
}
:deep(.custom-pagination .el-pager li) {
border-radius: 8px;
margin: 0 4px;
transition: all 0.2s;
min-width: 36px;
height: 36px;
line-height: 34px;
}
:deep(.custom-pagination .el-pager li:hover) {
transform: scale(1.05);
}
:deep(.custom-pagination .el-pager li.is-active) {
background: linear-gradient(135deg, #3b82f6, #6366f1);
color: white;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
/* 跳转输入框样式 */
:deep(.custom-pagination .el-pagination__jump input) {
border-radius: 8px;
border-color: #e5e7eb;
transition: border-color 0.2s;
}
:deep(.custom-pagination .el-pagination__jump input:focus) {
border-color: #3b82f6;
}
/* 页码选择器样式 */
:deep(.custom-pagination .el-pagination__sizes .el-input__wrapper) {
border-radius: 8px;
border-color: #e5e7eb;
transition: border-color 0.2s;
}
:deep(.custom-pagination .el-pagination__sizes .el-input__wrapper:hover) {
border-color: #3b82f6;
}
/* 总数显示样式 */
:deep(.custom-pagination .el-pagination__total) {
color: #6b7280;
font-weight: 500;
}
/* 回到顶部按钮动画 */
.back-top-btn:hover .w-8 {
animation: bounce-rotate 0.6s ease-in-out;
}
@keyframes bounce-rotate {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-4px) rotate(12deg);
}
}
</style> -->
defineExpose({
refresh: reset,
})
</script>
......@@ -2,9 +2,16 @@
<div class="page">
<div class="header h-40px items-center justify-between">
<div class="left flex gap-3 flex items-center">
<Tabs v-model="activeTab" :tabs="tabs" />
<Tabs
v-model="activeTab"
:tabs="tabs"
@change="(value) => handleTabChange(value as string)"
/>
<!-- 刷新图标 -->
<el-icon size="15" class="cursor-pointer hover:rotate-180 transition-all duration-300"
<el-icon
size="15"
class="cursor-pointer hover:rotate-180 transition-all duration-300"
@click="handleRefresh"
><Refresh
/></el-icon>
</div>
......@@ -12,8 +19,7 @@
<el-divider style="margin: 10px 0 20px 0" />
<!-- 主内容区域 -->
<transition name="fade" mode="out-in">
<RecommendList v-if="activeTab === '推荐' || activeTab === '最新'" />
<VideoList v-else />
<component ref="activeTabComponentRef" :is="activeTabComponent" />
</transition>
</div>
</template>
......@@ -24,11 +30,28 @@ import VideoList from './components/videoList.vue'
import Tabs from '@/components/common/Tabs'
const tabs = [
{ label: '推荐', value: '推荐' },
{ label: '最新', value: '最新' },
{ label: '视频', value: '视频' },
{ label: '推荐', value: '推荐', component: RecommendList },
{ label: '最新', value: '最新', component: RecommendList },
{ label: '视频', value: '视频', component: VideoList },
]
const activeTab = ref('推荐')
const activeTabComponent = computed(() => {
return tabs.find((tab) => tab.value === activeTab.value)?.component
})
const activeTabComponentRef =
useTemplateRef<InstanceType<typeof RecommendList>>('activeTabComponentRef')
const handleRefresh = () => {
activeTabComponentRef.value?.refresh?.()
}
const handleTabChange = (tab: string) => {
if (tab === '最新') {
activeTabComponentRef.value?.refresh?.()
}
}
</script>
<style lang="scss" scoped>
......
......@@ -47,6 +47,7 @@
<div class="right flex-col gap-3 xl:flex xl:basis-1/4 hidden">
<!-- 等级等相关信息 -->
<div
ref="levelContainerRef"
class="level-container common-box flex flex-col justify-center items-center gap-4 rounded-lg bg-#E4F5FE"
>
<div class="top flex items-center justify-center gap-3">
......@@ -81,16 +82,19 @@
</div>
</div>
<div class="flex flex-col sm:flex-row gap-2">
<div ref="dailySignBtnRef">
<el-button
class="bg-[linear-gradient(to_right,#FFD06A_0%,#FFB143_100%)] shadow-[0px_1px_8px_0_rgba(255,173,91,0.25)] border-none hover:-translate-y-1 hover:shadow-[0px_4px_10px_0_rgba(255,173,91,0.4)] hover:scale-105 active:scale-95 active:translate-y-0 transition-all duration-200 flex-1 text-xs sm:text-sm"
type="primary"
@click="onDailySign"
v-if="!userRecordData.isSign"
>
<!-- v-if="!userRecordData.isSign" -->
<svg-icon name="sign_in" size="35" />
<span class="text-black text-xs sm:text-sm">立即签到</span>
</el-button>
</div>
<el-button
class="bg-[linear-gradient(to_right,#ABB0FF_0%,#7495FF_100%)] shadow-[0_1px_8px_0_rgba(0,36,237,0.25)] border-none hover:-translate-y-1 transition-all duration-200 flex-1 text-xs sm:text-sm w-116px"
type="primary"
......@@ -209,8 +213,9 @@
<el-button
class="bg-[linear-gradient(to_right,#FFC5A1_0%,#FFB77F_100%)] shadow-[0_1px_8px_0_rgba(255,141,54,0.25)] border-none hover:-translate-y-1 transition-all duration-200 text-xs sm:text-sm rounded-full"
type="primary"
@click="handleTask(item)"
>
<span class="text-black text-xs sm:text-sm">签到</span>
<span class="text-black text-xs sm:text-sm">完成</span>
</el-button>
</div>
<!-- 分割线 -->
......@@ -238,10 +243,18 @@ import { getTaskList, dailySign, getCarouselList, getUserAccountData, getRecordD
import { TaskTypeEnum, TaskDateLimitTypeText } from '@/constants'
import type { CarouselItemDto, TaskItemDto, UserAccountDataDto, UserRecordDataDto } from '@/api'
import { TABS_REF_KEY, levelListOptions } from '@/constants'
import { useScrollTop, useHintAnimation } from '@/hooks'
const route = useRoute()
const router = useRouter()
const levelContainerRef = useTemplateRef<HTMLElement>('levelContainerRef')
const dailySignBtnRef = useTemplateRef<HTMLElement>('dailySignBtnRef')
const { handleBackTop } = useScrollTop(levelContainerRef)
const { triggerAnimation } = useHintAnimation(dailySignBtnRef, {
classes: ['scale-bounce', 'highlight', 'shake-x'],
})
const getThirdLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => {
// console.log(route, '三级路由')
return route.fullPath
......@@ -318,8 +331,11 @@ const onDailySign = async () => {
await dailySign()
}
const openPostCaseDialog = () => {
console.log('openPostCaseDialog')
const handleTask = (item: TaskItemDto) => {
// if (item.svgName === 'svgName') {
handleBackTop()
triggerAnimation()
// }
}
const initPage = () => {
......
......@@ -2,29 +2,98 @@
<div class="min-h-screen bg-gray-50">
<!-- 发布区域 -->
<div class="bg-white p-6 mb-6 rounded-lg shadow-sm">
<div class="flex items-start gap-3">
<el-avatar :size="40" src="/avatar.jpg" />
<div class="flex-1 bg-white rounded-lg border border-gray-200">
<!-- 主输入区域 -->
<div class="flex gap-3 mb-4">
<!-- 用户头像 -->
<el-avatar :size="48" :src="userInfo.avatar" class="flex-shrink-0">
<el-icon><User /></el-icon>
</el-avatar>
<!-- 输入区域 -->
<div class="flex-1">
<div class="text-gray-500 mb-2">添加话题</div>
<div class="text-gray-400 text-sm mb-4">分享你的企业文化实践案例......</div>
<!-- 话题标签输入 -->
<div class="mb-4">
<el-input
v-model="tagInput"
placeholder="话题描述...... (非必填)"
class="tag-input"
clearable
/>
</div>
<!-- 主要内容输入 -->
<div class="relative mb-3">
<el-input
type="textarea"
placeholder="请输入你想发布的话题"
:rows="3"
:maxlength="500"
resize="none"
class="main-textarea"
/>
<!-- 字符计数 -->
<div class="absolute bottom-3 right-3 text-xs text-gray-400">1/30</div>
</div>
</div>
</div>
<!-- 工具栏 -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-gray-400">
<i class="i-carbon-hashtag cursor-pointer hover:text-gray-600"></i>
<i class="i-carbon-face-satisfied cursor-pointer hover:text-gray-600"></i>
<i class="i-carbon-video cursor-pointer hover:text-gray-600"></i>
<i class="i-carbon-attachment cursor-pointer hover:text-gray-600"></i>
</div>
<div class="flex items-center justify-between pl-15">
<!-- 左侧工具按钮 -->
<div class="flex items-center gap-1">
<el-tooltip content="添加标签" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
>
<el-icon size="18"><CollectionTag /></el-icon>
</el-button>
</el-tooltip>
<div class="flex items-center gap-2">
<el-tag size="small" color="#f0f9ff" class="text-blue-600">仅粉丝可见</el-tag>
<el-tag size="small" color="#fef3c7" class="text-yellow-600">草稿</el-tag>
<el-tag size="small" color="#dbeafe" class="text-blue-600">发布动态</el-tag>
</div>
<el-tooltip content="添加图片" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
>
<el-icon size="18"><Picture /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="添加视频" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
>
<el-icon size="18"><VideoPlay /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="添加附件" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
>
<el-icon size="18"><Paperclip /></el-icon>
</el-button>
</el-tooltip>
</div>
<div class="text-right text-gray-400 text-sm mt-2">0/30</div>
<!-- 右侧操作按钮 -->
<div class="flex items-center gap-3">
<el-button
class="px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-50 rounded-lg border border-gray-200 text-sm"
>
存草稿
</el-button>
<el-button
type="primary"
:disabled="1"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
>
发布话题
</el-button>
</div>
</div>
</div>
</div>
......@@ -116,6 +185,11 @@
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
// 标签数据
const tags = ref([
{ name: '最新', active: true },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment