Commit 4a950630 by lijiabin

【需求 17679】 wip: 继续完善页面

parent 2610f8e0
......@@ -53,6 +53,7 @@ declare module 'vue' {
IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
LoadingComponent: typeof import('./src/components/common/LoadingComponent/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SelectTags: typeof import('./src/components/common/SelectTags/index.vue')['default']
......@@ -108,6 +109,7 @@ declare global {
const IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
const IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
const IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
const LoadingComponent: typeof import('./src/components/common/LoadingComponent/index.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView']
const SelectTags: typeof import('./src/components/common/SelectTags/index.vue')['default']
......
......@@ -42,10 +42,11 @@ interface AddOrUpdateColumnBase {
faceUrl?: string
imgUrl?: string
// 关联的专栏栏目
relateColumnId: number
relateColumnId?: number
mainTagId: string
isRelateColleague: BooleanFlag
sendTime?: string
isRecommend: BooleanFlag
}
/**
......@@ -72,9 +73,10 @@ export interface AddOrUpdateInterviewBase {
faceUrl?: string
imgUrl?: string
// 关联的专访栏目
relateColumnId: number
relateColumnId?: number
mainTagId: string
sendTime?: string
isRecommend: BooleanFlag
}
export interface AddOrUpdateInterviewForm extends AddOrUpdateInterviewBase {
......
import service from '@/utils/request/index'
import type { AddOrUpdatePracticeDto, ArticleItemDto, ArticleSearchParams } from './types'
import type { AddOrUpdatePracticeDto, PracticeSearchParams, PracticeItemDto } from './types'
import type { BackendServicePageResult } from '@/utils/request/types'
import { ArticleTypeEnum } from '@/constants'
// 关于实践相关接口
......@@ -15,3 +14,14 @@ export const addOrUpdatePractice = (data: AddOrUpdatePracticeDto) => {
data,
})
}
/**
* 实践列表接口
*/
export const getPracticeList = (data: PracticeSearchParams) => {
return service.request<BackendServicePageResult<PracticeItemDto>>({
url: '/api/yaCulture/practiceList',
method: 'POST',
data,
})
}
......@@ -20,8 +20,8 @@ export interface AddOrUpdatePracticeDto {
/**
* 搜索文章的参数
*/
export interface ArticleSearchParams extends PageSearchParams {
type?: ArticleTypeEnum
export interface PracticeSearchParams extends PageSearchParams {
sortLogic?: number
}
/**
......@@ -90,3 +90,27 @@ export interface ArticleItemDto {
replyCount: number
hasPraised: BooleanFlag
}
/**
* 实践列表item
*/
export interface PracticeItemDto {
collectionCount: number
content: string
createTime: number
createUserId: number
description: string
faceUrl: string
hasPraised: boolean
id: number
isRecommend: BooleanFlag
praiseCount: number
releaseStatus: ReleaseStatusTypeEnum
replyCount: number
showAvatar: string
showName: string
tagNameList: string[]
title: string
type: ArticleTypeEnum.PRACTICE
viewCount: number
}
<!-- LoadingComponent.vue -->
<template>
<div class="loading-container">
<div class="loading-spinner"></div>
<span v-if="showText" class="loading-text">{{ text }}</span>
</div>
</template>
<script setup lang="ts">
interface Props {
text?: string
showText?: boolean
}
withDefaults(defineProps<Props>(), {
text: '加载中...',
showText: true,
})
</script>
<style scoped>
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
gap: 12px;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f0f0f0;
border-top-color: #409eff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.loading-text {
font-size: 14px;
color: #909399;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
......@@ -24,8 +24,9 @@ export default defineComponent((_, { expose }) => {
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
isRelateColleague: BooleanFlag.NO,
relateColumnId: 0,
relateColumnId: undefined,
type: ArticleTypeEnum.COLUMN,
isRecommend: BooleanFlag.NO,
})
const formRef = ref<InstanceType<typeof ElForm>>()
const rules = {
......@@ -38,6 +39,7 @@ export default defineComponent((_, { expose }) => {
relateColumnId: [
{ required: true, message: '请选择专栏栏目', trigger: 'blur', type: 'number', min: 1 },
],
isRecommend: [{ required: true, message: '请选择是否推荐', trigger: 'blur' }],
}
const transformForm = (releaseStatus: ReleaseStatusTypeEnum): AddOrUpdateColumnDto => {
......@@ -113,11 +115,11 @@ export default defineComponent((_, { expose }) => {
<UploadFile v-model={form.value.imgUrl} />
</el-form-item>
<el-form-item label="专栏栏目选择" prop="relateColumnId">
<el-radio-group v-model={form.value.relateColumnId}>
<el-select v-model={form.value.relateColumnId} placeholder="请选择专栏栏目">
{columnList.value.map((item) => (
<el-radio value={item.id}>{item.title}</el-radio>
<el-option value={item.id}>{item.title}</el-option>
))}
</el-radio-group>
</el-select>
</el-form-item>
<el-form-item label="主标签" prop="mainTagId">
{{
......@@ -126,7 +128,7 @@ export default defineComponent((_, { expose }) => {
label: () => (
// <el-tooltip content="主标签最多选1个" placement="top">
<span class="cursor-pointer">
标签
标签
{/* <el-icon class="ml-1">
<InfoFilled />
</el-icon> */}
......@@ -167,6 +169,16 @@ export default defineComponent((_, { expose }) => {
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否推荐" prop="isRecommend">
<el-radio-group v-model={form.value.isRecommend} class="radio-group">
<el-radio value={BooleanFlag.YES} class="radio-item immediate">
</el-radio>
<el-radio value={BooleanFlag.NO} class="radio-item scheduled">
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="发布类型" prop="sendType">
<el-radio-group v-model={form.value.sendType} class="radio-group">
<el-radio value={SendTypeEnum.IMMEDIATE} class="radio-item immediate">
......@@ -184,6 +196,12 @@ export default defineComponent((_, { expose }) => {
v-model={form.value.sendTime}
type="datetime"
placeholder="请选择发布时间"
// 不能选现在
disabled-date={(time: Date) => {
return time.getTime() < Date.now() - 1000 * 60 * 60 * 24
}}
value-format="X"
style={{ width: '250px' }}
/>
</el-form-item>
)}
......
import { ArticleTypeEnum, ReleaseStatusTypeEnum, SendTypeEnum } from '@/constants'
import { ArticleTypeEnum, BooleanFlag, ReleaseStatusTypeEnum, SendTypeEnum } from '@/constants'
import UploadFile from '@/components/common/UploadFile/index.vue'
import { useResetData } from '@/hooks'
import { useColumnStore } from '@/stores/column'
import { useInterviewStore } from '@/stores/interview'
import { storeToRefs } from 'pinia'
import type { AddOrUpdateInterviewDto, AddOrUpdateInterviewForm } from '@/api/article/types'
import type { TagItemDto } from '@/api/tag/types'
export default defineComponent((_, { expose }) => {
const columnStore = useColumnStore()
const { columnList } = storeToRefs(columnStore)
const interviewStore = useInterviewStore()
const { interviewList } = storeToRefs(interviewStore)
const [form, resetForm] = useResetData<AddOrUpdateInterviewForm>({
title: '',
content: '',
......@@ -22,7 +22,8 @@ export default defineComponent((_, { expose }) => {
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
type: ArticleTypeEnum.INTERVIEW,
relateColumnId: 0,
relateColumnId: undefined,
isRecommend: BooleanFlag.NO,
})
const formRef = ref<InstanceType<typeof ElForm>>()
const rules = {
......@@ -35,6 +36,7 @@ export default defineComponent((_, { expose }) => {
relateColumnId: [
{ required: true, message: '请选择专访栏目', trigger: 'blur', type: 'number', min: 1 },
],
isRecommend: [{ required: true, message: '请选择是否推荐', trigger: 'blur' }],
}
const transformForm = (releaseStatus: ReleaseStatusTypeEnum): AddOrUpdateInterviewDto => {
......@@ -110,11 +112,11 @@ export default defineComponent((_, { expose }) => {
<UploadFile v-model={form.value.imgUrl} />
</el-form-item>
<el-form-item label="专访栏目选择" prop="relateColumnId">
<el-radio-group v-model={form.value.relateColumnId}>
{columnList.value.map((item) => (
<el-radio value={item.id}>{item.title}</el-radio>
<el-select v-model={form.value.relateColumnId} placeholder="请选择专访栏目">
{interviewList.value.map((item) => (
<el-option value={item.id} label={item.title} />
))}
</el-radio-group>
</el-select>
</el-form-item>
<el-form-item label="主标签" prop="mainTagId">
{{
......@@ -123,7 +125,7 @@ export default defineComponent((_, { expose }) => {
label: () => (
// <el-tooltip content="主标签最多选1个" placement="top">
<span class="cursor-pointer">
标签
标签
{/* <el-icon class="ml-1">
<InfoFilled />
</el-icon> */}
......@@ -154,6 +156,17 @@ export default defineComponent((_, { expose }) => {
),
}}
</el-form-item>
<el-form-item label="是否推荐" prop="isRecommend">
<el-radio-group v-model={form.value.isRecommend} class="radio-group">
<el-radio value={BooleanFlag.YES} class="radio-item immediate">
</el-radio>
<el-radio value={BooleanFlag.NO} class="radio-item scheduled">
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="发布类型" prop="sendType">
<el-radio-group v-model={form.value.sendType} class="radio-group">
<el-radio value={SendTypeEnum.IMMEDIATE} class="radio-item immediate">
......@@ -171,6 +184,12 @@ export default defineComponent((_, { expose }) => {
v-model={form.value.sendTime}
type="datetime"
placeholder="请选择发布时间"
// 不能选现在
disabled-date={(time: Date) => {
return time.getTime() < Date.now() - 1000 * 60 * 60 * 24
}}
value-format="X"
style={{ width: '250px' }}
/>
</el-form-item>
)}
......
......@@ -100,7 +100,12 @@ export default defineComponent(
v-model={form.value.sendTime}
type="datetime"
placeholder="请选择发布时间"
// 不能选现在
disabled-date={(time: Date) => {
return time.getTime() < Date.now() - 1000 * 60 * 60 * 24
}}
value-format="X"
style={{ width: '250px' }}
/>
</el-form-item>
)}
......
......@@ -109,7 +109,7 @@ export default defineComponent((_, { expose }) => {
label: () => (
// <el-tooltip content="主标签最多选1个" placement="top">
<span class="cursor-pointer">
标签
标签
{/* <el-icon class="ml-1">
<InfoFilled />
</el-icon> */}
......@@ -157,6 +157,12 @@ export default defineComponent((_, { expose }) => {
v-model={form.value.sendTime}
type="datetime"
placeholder="请选择发布时间"
// 不能选现在
disabled-date={(time: Date) => {
return time.getTime() < Date.now() - 1000 * 60 * 60 * 24
}}
value-format="X"
style={{ width: '250px' }}
/>
</el-form-item>
)}
......
......@@ -40,47 +40,84 @@ import type { Component } from 'vue'
// import { Plus } from '@element-plus/icons-vue'
import { addOrUpdateArticle, addOrUpdatePractice } from '@/api'
import { ArticleTypeEnum, ReleaseStatusTypeEnum } from '@/constants'
import PostForm from './postForm.tsx'
import PracticeForm from './practiceForm.tsx'
import LoadingComponent from '@/components/common/LoadingComponent/index.vue'
const typeMap: Record<
ArticleTypeEnum,
{ title: string; component: Component; api?: (data: any) => Promise<any> }
> = {
[ArticleTypeEnum.VIDEO]: {
title: '视频',
component: PostForm,
component: defineAsyncComponent({
loader: () => import('./postForm.tsx'),
delay: 200,
loadingComponent: LoadingComponent,
}),
},
[ArticleTypeEnum.QUESTION]: {
title: '问题',
component: PostForm,
component: defineAsyncComponent({
loader: () => import('./postForm.tsx'),
delay: 200,
loadingComponent: LoadingComponent,
}),
},
[ArticleTypeEnum.POST]: {
title: '帖子',
component: defineAsyncComponent(() => import('./postForm.tsx')),
component: defineAsyncComponent({
loader: async () => {
const start = Date.now()
const comp = await import('./postForm.tsx')
const cost = Date.now() - start
console.log('cost', cost)
const min = 1000 // 最低 200ms
if (cost < min) {
await new Promise((r) => setTimeout(r, min - cost))
}
return comp
},
delay: 200,
loadingComponent: LoadingComponent,
}),
api: addOrUpdateArticle,
},
[ArticleTypeEnum.PRACTICE]: {
title: '实践',
component: defineAsyncComponent(() => import('./practiceForm.tsx')),
component: defineAsyncComponent({
loader: () => import('./practiceForm.tsx'),
delay: 200,
loadingComponent: LoadingComponent,
}),
api: addOrUpdatePractice,
},
[ArticleTypeEnum.COLUMN]: {
title: '专栏',
component: defineAsyncComponent(() => import('./colnumForm.tsx')),
component: defineAsyncComponent({
loader: () => import('./colnumForm.tsx'),
delay: 200,
loadingComponent: LoadingComponent,
}),
api: addOrUpdateArticle,
},
[ArticleTypeEnum.INTERVIEW]: {
title: '专访',
component: defineAsyncComponent(() => import('./interviewForm.tsx')),
component: defineAsyncComponent({
loader: () => import('./interviewForm.tsx'),
delay: 200,
loadingComponent: LoadingComponent,
}),
api: addOrUpdateArticle,
},
}
const dialogTitle = computed(() => '发布' + typeMap[articleType.value].title)
const formComponentRef =
useTemplateRef<InstanceType<typeof PostForm | typeof PracticeForm>>('formComponentRef')
export interface BaseFormExpose {
resetFields: () => void
getValidatedFormData: (releaseStatus: ReleaseStatusTypeEnum) => Promise<unknown>
}
const formComponentRef = useTemplateRef<BaseFormExpose>('formComponentRef')
const currentFormComp = computed(() => {
return typeMap[articleType.value].component
......@@ -100,7 +137,7 @@ const open = (type: ArticleTypeEnum) => {
// 关闭弹窗
const close = () => {
dialogVisible.value = false
formComponentRef.value?.resetFields()
formComponentRef?.value?.resetFields()
}
const handleClosed = () => {
......
......@@ -16,8 +16,8 @@
<!-- 搜索框 -->
<div class="flex-1 max-w-sm mx-4 hidden md:block lg:max-w-md lg:mx-6">
<el-input v-model="search" class="h-8" placeholder="搜索">
<template #prefix>
<el-icon class="text-gray-400">
<template #suffix>
<el-icon class="text-gray-400" @click="router.push('/searchPage')">
<Search />
</el-icon>
</template>
......
......@@ -66,6 +66,12 @@ const routes = [
name: 'CulturePublishCase',
component: () => import('@/views/publishCase/index.vue'),
},
// 搜索页面
{
path: 'searchPage',
name: 'CultureSearchPage',
component: () => import('@/views/searchPage/index.vue'),
},
// 发布文章
// {
// {
......
......@@ -128,6 +128,7 @@ const { list, total, searchParams, loading, goToPage, changePageSize, refresh }
defaultParams: { sortLogic: 0 },
defaultCurrent: 1,
defaultSize: 5,
immediate: false,
},
)
const tabsRef = inject(TABS_REF_KEY)
......@@ -135,7 +136,7 @@ const tabsRef = inject(TABS_REF_KEY)
const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
defineExpose({
refresh: (sortLogic: number) => {
refresh: (sortLogic?: number) => {
console.log('sortLogic', sortLogic)
searchParams.value.sortLogic = sortLogic
refresh()
......
......@@ -381,6 +381,7 @@ const { list, total, searchParams, loading, goToPage, changePageSize, refresh }
defaultParams: { type: ArticleTypeEnum.VIDEO },
defaultCurrent: 1,
defaultSize: 12,
immediate: false,
},
)
// Tabs 配置
......@@ -397,8 +398,7 @@ const goVideoDetail = (n: number) => {
router.push(`/videoDetail?id=${n}`)
}
defineExpose({
refresh: (sortLogic: number) => {
searchParams.value.sortLogic = sortLogic
refresh: () => {
refresh()
},
})
......
......@@ -2,11 +2,8 @@
<div>
<div class="header h-40px items-center justify-between">
<div class="left flex gap-3 flex items-center">
<Tabs
v-model="activeTab"
:tabs="tabs"
@change="(value) => handleTabChange(value as string)"
/>
<Tabs v-model="activeTab" :tabs="tabs" />
<!-- 直接在@change里面拿最新的动态组件实例 拿不到 可以在@enter 动画钩子里面拿 -->
<!-- 刷新图标 -->
<el-icon
size="15"
......@@ -18,8 +15,11 @@
</div>
<el-divider style="margin: 10px 0 20px 0" />
<!-- 主内容区域 -->
<transition name="fade" mode="out-in">
<component ref="activeTabComponentRef" :is="activeTabComponent" />
<!-- 同一个组件 不同的实例 -->
<transition name="fade" mode="out-in" @enter="handleEnter">
<keep-alive>
<component ref="activeTabComponentRef" :is="activeTabComponent" :key="activeTab" />
</keep-alive>
</transition>
</div>
</template>
......@@ -40,8 +40,9 @@ const activeTabComponent = computed(() => {
return tabs.find((tab) => tab.value === activeTab.value)?.component
})
const activeTabComponentRef =
useTemplateRef<InstanceType<typeof RecommendList>>('activeTabComponentRef')
const activeTabComponentRef = useTemplateRef<
InstanceType<typeof RecommendList> | InstanceType<typeof VideoList>
>('activeTabComponentRef')
const handleRefresh = () => {
if (activeTab.value === '推荐') {
......@@ -49,17 +50,17 @@ const handleRefresh = () => {
} else if (activeTab.value === '最新') {
activeTabComponentRef.value?.refresh?.(1)
} else if (activeTab.value === '视频') {
activeTabComponentRef.value?.refresh?.(2)
activeTabComponentRef.value?.refresh?.()
}
}
const handleTabChange = (tab: string) => {
if (tab === '最新') {
activeTabComponentRef.value?.refresh?.(1)
} else if (tab === '推荐') {
activeTabComponentRef.value?.refresh?.(0)
}
const handleEnter = () => {
handleRefresh()
}
onMounted(() => {
handleRefresh()
})
</script>
<style lang="scss" scoped>
......
......@@ -17,8 +17,7 @@
{{ item.title }}
</h3>
<div class="flex items-center cursor-pointer">
<span class="mr-1 text-14px color-#606266">查看更多</span>
<el-icon><ArrowRight /></el-icon>
<span class="mr-1 text-14px color-#606266">查看更多 >></span>
</div>
</div>
......@@ -35,10 +34,12 @@
:src="i.faceUrl"
class="w-full aspect-[5/3] object-cover group-hover:scale-105 transition-transform duration-300"
/>
<div class="absolute top-2 left-2">
<el-tag size="small" class="bg-orange-500 text-white border-none">
1111111
</el-tag>
<div
v-if="i.isRecommend"
class="absolute top-0 left-0 w-15 h-7 z-1000 bg-#FFF9B9 flex items-center justify-center border-2px border-solid border-#f4f0eb rounded-tl-lg rounded-br-lg"
>
<img class="w-6" src="@/assets/img/culture/recommend.png" alt="" />
<div class="text-12px text-#000 line-height-12px">推荐</div>
</div>
</div>
<h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors">
......
......@@ -18,9 +18,8 @@
</h3>
<div class="flex items-center cursor-pointer hover:text-[var(--dynamic-color)]">
<span class="mr-1 text-14px color-#606266 hover:text-[var(--dynamic-color)]"
>查看更多</span
>查看更多 >></span
>
<el-icon><ArrowRight /></el-icon>
</div>
</div>
......@@ -35,12 +34,14 @@
<div class="relative mb-3 overflow-hidden rounded-lg">
<img
:src="i.faceUrl"
class="w-full h-32 object-cover group-hover:scale-105 transition-transform duration-300"
class="w-full aspect-[5/3] object-cover group-hover:scale-105 transition-transform duration-300"
/>
<div class="absolute top-2 left-2">
<el-tag size="small" class="bg-orange-500 text-white border-none">
1111111
</el-tag>
<div
v-if="i.isRecommend"
class="absolute top-0 left-0 w-15 h-7 z-1000 bg-#FFF9B9 flex items-center justify-center border-2px border-solid border-#f4f0eb rounded-tl-lg rounded-br-lg"
>
<img class="w-6" src="@/assets/img/culture/recommend.png" alt="" />
<div class="text-12px text-#000 line-height-12px">推荐</div>
</div>
</div>
<h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors">
......
<template>
<div class="min-h-screen bg-gray-50">
<div class="min-h-screen">
<!-- 发布区域 -->
<div class="bg-white p-6 mb-6 rounded-lg shadow-sm">
<div class="flex-1 bg-white rounded-lg border border-gray-200">
<!-- 主输入区域 -->
<div class="flex gap-3 mb-4">
<div class="flex gap-3 mb-4 items-center">
<!-- 用户头像 -->
<el-avatar :size="48" :src="userInfo.avatar" class="flex-shrink-0">
<el-icon><User /></el-icon>
......@@ -88,7 +88,7 @@
<el-button
type="primary"
:disabled="1"
:disabled="!tagInput"
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"
>
发布话题
......@@ -100,16 +100,28 @@
<!-- 标签导航 -->
<div class="bg-white p-4 mb-6 rounded-lg shadow-sm">
<div class="flex flex-wrap gap-2 mb-2">
<el-tag
v-for="tag in filterOptions"
:key="tag.id"
:type="tag.id === searchParams.sortLogic ? 'primary' : 'info'"
:effect="tag.id === searchParams.sortLogic ? 'dark' : 'plain'"
class="cursor-pointer"
@click="toggleFilter(tag.id)"
>
{{ tag.title }}
</el-tag>
</div>
<div class="flex flex-wrap gap-2">
<el-tag
v-for="tag in tags"
:key="tag.name"
:type="tag.active ? 'primary' : ''"
:effect="tag.active ? 'dark' : 'plain'"
v-for="tag in tagList"
:key="tag.id"
:type="activeTag === tag.id ? 'primary' : 'info'"
:effect="activeTag === tag.id ? 'dark' : 'plain'"
class="cursor-pointer"
@click="handleTagClick(tag)"
@click="activeTag = tag.id"
>
{{ tag.name }}
{{ tag.title }}
</el-tag>
</div>
</div>
......@@ -120,62 +132,108 @@
<div class="flex items-center justify-between p-4 border-b border-gray-100">
<div class="flex items-center gap-2">
<div class="w-1 h-6 bg-red-500 rounded"></div>
<h2 class="text-lg font-medium">最新</h2>
<h2 class="text-lg font-medium">
{{ tagList.find((tag) => tag.id === activeTag)?.title ?? '最新' }}
</h2>
</div>
<div
class="text-#999 cursor-pointer text-sm"
@click="router.push(`/userPage?key=practice`)"
>
查看更多 >>
</div>
<div class="text-blue-600 text-sm cursor-pointer hover:text-blue-700">查看更多 >></div>
</div>
<!-- 动态列表 -->
<div class="divide-y divide-gray-100">
<div v-for="post in posts" :key="post.id" class="p-4 hover:bg-gray-50 transition-colors">
<div class="flex gap-3">
<div
@click="router.push(`/postDetail/${item.id}`)"
v-for="item in list"
:key="item.id"
class="p-4 hover:bg-gray-50 transition-colors cursor-pointer"
>
<div class="flex gap-3 items-center">
<!-- 左侧内容 -->
<div class="flex-1">
<h3 class="text-base font-medium text-gray-900 mb-2 leading-relaxed">
{{ post.title }}
{{ item.title }}
</h3>
<!-- 带图片的内容 -->
<div v-if="post.image" class="flex gap-3 mb-3">
<div class="flex gap-3 mb-3">
<img
:src="post.image"
:alt="post.title"
v-if="item.faceUrl"
:src="item.faceUrl"
:alt="item.title"
class="w-20 h-20 object-cover rounded-lg flex-shrink-0"
/>
<div class="flex-1">
<div class="text-gray-600 text-sm leading-relaxed">
{{ post.description }}
{{ item.content }}
</div>
</div>
</div>
<!-- 无图片的内容 -->
<div v-else class="text-gray-600 text-sm leading-relaxed mb-3">
{{ post.description }}
</div>
<!-- <div v-else class="text-gray-600 text-sm leading-relaxed mb-3">
{{ item.content }}
</div> -->
<!-- 互动数据 -->
<div class="flex items-center gap-6 text-gray-400 text-sm">
<div class="flex items-center gap-4 text-gray-400 text-sm">
<div class="flex items-center gap-1">
<i class="i-carbon-view text-base"></i>
<span>{{ post.views }}</span>
<el-icon class="text-sm"><View /></el-icon>
<span>{{ item.viewCount }}</span>
</div>
<div class="flex items-center gap-1">
<i class="i-carbon-favorite text-base"></i>
<span>{{ post.likes }}</span>
<el-icon class="text-sm"><ChatDotRound /></el-icon>
<span>{{ item.replyCount }}</span>
</div>
<div class="flex items-center gap-1">
<i class="i-carbon-chat text-base"></i>
<span>{{ post.comments }}</span>
<el-icon class="text-sm"><Star /></el-icon>
<span>{{ item.praiseCount }}</span>
</div>
<div class="ml-auto">{{ post.time }}</div>
<div>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm') }}</div>
</div>
</div>
<!-- 右侧头像 -->
<div class="flex flex-col items-center gap-1 flex-shrink-0">
<el-avatar :size="40" :src="post.avatar" />
<div class="text-xs text-gray-500">{{ post.author }}</div>
<el-avatar :size="40" :src="item.showAvatar" />
<div class="text-xs text-gray-500">{{ item.showName }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部分页 -->
<div class="bottom-pagination backdrop-blur-8 border-t border-gray-200">
<div class="max-w-7xl mx-auto py-6">
<div class="flex items-center justify-between">
<!-- 左侧:回到顶部按钮 -->
<div class="left">
<ScrollTopComp />
</div>
<!-- 右侧:分页器 -->
<div class="right">
<div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"
>
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:page-sizes="[5, 20, 1]"
layout="prev, pager, next, jumper, total"
:total="total"
class="custom-pagination"
@current-change="
(e) => {
;(handleBackTop(), goToPage(e))
}
"
@size-change="changePageSize"
/>
</div>
</div>
</div>
......@@ -187,90 +245,45 @@
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
import { getPracticeList } from '@/api/practice'
import { usePageSearch, useScrollTop } from '@/hooks'
import dayjs from 'dayjs'
import { TABS_REF_KEY } from '@/constants'
import { useTagsStore } from '@/stores/tags'
const tagsStore = useTagsStore()
const { tagList } = storeToRefs(tagsStore)
const activeTag = ref(tagList.value[0]?.id ?? 0)
const { userInfo } = storeToRefs(userStore)
const router = useRouter()
const tabsRef = inject(TABS_REF_KEY)
const { userInfo } = storeToRefs(useUserStore())
// 标签数据
const tags = ref([
{ name: '最新', active: true },
{ name: '最热', active: false },
{ name: '商务观看', active: false },
{ name: '多收藏', active: false },
{ name: 'Amazon产品开发部', active: false },
{ name: 'Amazon销售部', active: false },
{ name: 'Amazon运营部', active: false },
{ name: '领导设计部', active: false },
{ name: '知识产权部', active: false },
{ name: '供应链管理部', active: false },
{ name: 'IT技术部', active: false },
{ name: '财务部', active: false },
{ name: '人力资源中心', active: false },
const filterOptions = ref([
{ title: '最新', id: 0 },
{ title: '最热', id: 1 },
{ title: '最新观看', id: 2 },
])
// 动态数据
const posts = ref([
{
id: 1,
title: 'YA文化|郭修启示录——名门正派还在编说明书,野路子已经满天飞了',
description:
'最近听布谷鸟在网上聊了很多的郭修启示录,经过充分思考后发现这个问题,明明大家都是做好的!(☆_☆)',
image: null,
views: '1068',
likes: '94',
comments: '66',
time: '2023-08-20 17:00',
author: 'AAA超哥',
avatar: '/avatar1.jpg',
},
{
id: 2,
title: 'YA文化|郭修启示录——名门正派还在编说明书,野路子已经满天飞了',
description:
'最近听布谷鸟在网上聊了几个月的郭修启示录,经过充分思考后发现这个问题,明明大家都是做好的!(☆_☆) 但为什么总是有人在编说明书,而野路子已经满天飞了呢?这个问题值得我们深思。在这个快速发展的时代,我们需要更加灵活的思维方式,不能总是按部就班地按照传统的方式来做事情。',
image: '/post-image.jpg',
views: '1068',
likes: '94',
comments: '66',
time: '2023-08-20 17:00',
author: 'AAA超哥',
avatar: '/avatar2.jpg',
},
{
id: 3,
title: 'YA文化|郭修启示录——名门正派还在编说明书,野路子已经满天飞了',
description:
'最近听布谷鸟在网上聊了几个月的郭修启示录,经过充分思考后发现这个问题,明明大家都是做好的!(☆_☆)',
image: null,
views: '1068',
likes: '94',
comments: '66',
time: '2023-08-20 17:00',
author: 'AAA超哥',
avatar: '/avatar3.jpg',
},
const tagInput = ref('')
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const { list, total, searchParams, goToPage, changePageSize, refresh } = usePageSearch(
getPracticeList,
{
id: 4,
title: 'YA文化|郭修启示录——名门正派还在编说明书,野路子已经满天飞了',
description:
'最近听布谷鸟在网上聊了几个月的郭修启示录,经过充分思考后发现这个问题,明明大家都是做好的!(☆_☆) 但为什么总是有人在编说明书,而野路子已经满天飞了呢?这个问题值得我们深思。',
image: '/post-image2.jpg',
views: '1068',
likes: '94',
comments: '66',
time: '2023-08-20 17:00',
author: 'AAA超哥',
avatar: '/avatar4.jpg',
defaultParams: { sortLogic: filterOptions.value[0]?.id },
defaultCurrent: 1,
defaultSize: 5,
},
])
)
// 标签点击事件
const handleTagClick = (clickedTag) => {
tags.value.forEach((tag) => {
tag.active = tag.name === clickedTag.name
})
const toggleFilter = (id: number) => {
searchParams.value.sortLogic = id
refresh()
handleBackTop()
}
</script>
<style scoped>
/* 如果需要自定义样式可以在这里添加 */
</style>
<style scoped></style>
......@@ -143,7 +143,13 @@
type="datetime"
placeholder="选择发布时间"
size="small"
value-format="timestamp"
value-format="X"
:disabled-date="
(time: Date) => {
return time.getTime() < Date.now() - 1000 * 60 * 60 * 24
}
"
style="width: 250px"
/>
<el-switch
v-model="form.sendType"
......
<template>
<div class="min-h-screen bg-white">
<div class="max-w-1400px mx-auto p-6">
<!-- 搜索栏 -->
<div class="mb-8">
<div class="relative flex items-center gap-3">
<el-input
v-model="searchKeyword"
placeholder="输入关键词搜索"
size="large"
class="search-input flex-1"
@keyup.enter="handleSearch"
/>
<el-button type="primary" size="large" class="search-btn" @click="handleSearch">
搜索
</el-button>
</div>
</div>
<!-- 分类 Tabs -->
<div class="mb-6 flex gap-3 flex items-center">
<Tabs
v-model="activeTab"
:tabs="tabs"
@change="(value) => handleTabChange(value as string)"
/>
<button
v-for="sort in sortOptions"
:key="sort.value"
class="px-4 py-1.5 rounded-lg text-14px transition-colors"
:class="{
'text-blue-600 bg-blue-50': activeSort === sort.value,
'text-gray-600 hover:text-blue-600': activeSort !== sort.value,
}"
@click="activeSort = sort.value"
>
{{ sort.label }}
</button>
</div>
<!-- 排序方式 -->
<div class="flex items-center justify-between mb-6 py-4 border-b border-gray-200">
<div class="flex items-center gap-2">
<span class="text-gray-600 text-14px">排序方式:</span>
<div class="flex gap-2">
<button
v-for="sort in sortOptions"
:key="sort.value"
class="px-4 py-1.5 rounded-lg text-14px transition-colors"
:class="{
'text-blue-600 bg-blue-50': activeSort === sort.value,
'text-gray-600 hover:text-blue-600': activeSort !== sort.value,
}"
@click="activeSort = sort.value"
>
{{ sort.label }}
</button>
</div>
</div>
<div class="text-gray-500 text-14px">
共找到 <span class="text-blue-600 font-600">{{ totalResults }}</span> 条结果
</div>
</div>
<!-- 搜索结果列表 -->
<div class="space-y-4">
<div
v-for="item in searchResults"
:key="item.id"
class="flex gap-4 p-4 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer"
>
<!-- 封面图 -->
<div
class="flex-shrink-0 w-240px h-135px rounded-lg overflow-hidden bg-gray-100 relative"
>
<img :src="item.cover" class="w-full h-full object-cover" />
<div
class="absolute bottom-2 right-2 bg-black/70 text-white text-12px px-2 py-0.5 rounded"
>
{{ item.duration }}
</div>
</div>
<!-- 内容信息 -->
<div class="flex-1 flex flex-col justify-between min-w-0">
<div>
<!-- 标题 -->
<h3 class="text-16px font-500 text-gray-900 mb-2 line-clamp-1">
{{ item.title }}
</h3>
<!-- 描述 -->
<p class="text-14px text-gray-600 mb-3 line-clamp-2">
{{ item.description }}
</p>
</div>
<!-- 底部信息 -->
<div class="flex items-center gap-4 text-13px text-gray-500">
<span>{{ item.author }}</span>
<span>{{ item.views }}</span>
<span>{{ item.time }}</span>
</div>
</div>
<!-- 右侧视频标签 -->
<div class="flex-shrink-0 self-end">
<span class="text-blue-600 text-13px">视频</span>
</div>
</div>
</div>
<!-- 分页 -->
<div class="mt-8 flex justify-center">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="totalResults"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Tabs from '@/components/common/Tabs'
interface SearchResult {
id: number
title: string
description: string
cover: string
duration: string
author: string
views: string
time: string
}
const searchKeyword = ref('wayward')
const tabs = [
{ label: '综合', value: '综合', count: 189 },
{ label: '视频', value: '视频', count: 89 },
{ label: '帖子', value: '帖子', count: 0 },
{ label: '实践', value: '实践', count: 0 },
{ label: '专栏', value: '专栏', count: 0 },
{ label: '专访', value: '专访', count: 51 },
{ label: '问题', value: '问题', count: 0 },
]
const activeTab = ref('综合')
const sortOptions = ref([
{ label: '综合排序', value: 'default' },
{ label: '最多播放', value: 'views' },
{ label: '最新发布', value: 'time' },
{ label: '最多收藏', value: 'favorite' },
])
const activeSort = ref('default')
const totalResults = ref(189)
const currentPage = ref(1)
const pageSize = ref(20)
const searchResults = ref<SearchResult[]>([
{
id: 1,
title: 'wayward:已经20小时了!第3次挑战灭火了',
description: 'wayward这次挑战能否成功?经历了前两次失败后,这次能否成功灭火?让我们拭目以待...',
cover: 'https://picsum.photos/400/225?random=1',
duration: '20:45',
author: 'waywardzz',
views: '1.2万',
time: '2天前',
},
{
id: 2,
title: 'wayward:只是喝成这样还怎么玩?长江水果实太快了',
description: '这期节目真的太搞笑了,wayward的状态实在是...',
cover: 'https://picsum.photos/400/225?random=2',
duration: '15:30',
author: 'waywardzz',
views: '8956',
time: '3天前',
},
{
id: 3,
title: 'wayward生存挑战20小时记录',
description: '完整记录wayward的20小时生存挑战过程,从开始到结束的所有精彩瞬间...',
cover: 'https://picsum.photos/400/225?random=3',
duration: '1:20:15',
author: 'waywardFan',
views: '5.6万',
time: '1周前',
},
])
const handleSearch = () => {
console.log('搜索关键词:', searchKeyword.value)
}
const handleTabChange = (value: string) => {
console.log('切换分类:', value)
activeTab.value = value
}
</script>
<style scoped>
:deep(.search-input .el-input__wrapper) {
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
}
:deep(.search-btn) {
background: #3b82f6;
border: none;
border-radius: 0.5rem;
padding: 0 2rem;
}
.line-clamp-1 {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
......@@ -50,8 +50,8 @@ export default defineConfig({
port: 3000,
proxy: {
'/api1': {
// target: 'http://192.168.2.168:8089', // 立鹏本地
target: 'http://192.168.2.55:8089', // 首拥本地
target: 'http://192.168.2.168:8089', // 立鹏本地
// target: 'http://192.168.2.55:8089', // 首拥本地
changeOrigin: true,
rewrite: (path) => {
return path.replace(/^\/api1/, '')
......
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