Commit 4a950630 by lijiabin

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

parent 2610f8e0
...@@ -53,6 +53,7 @@ declare module 'vue' { ...@@ -53,6 +53,7 @@ declare module 'vue' {
IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default'] IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default'] IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
IconTooling: typeof import('./src/components/icons/IconTooling.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'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SelectTags: typeof import('./src/components/common/SelectTags/index.vue')['default'] SelectTags: typeof import('./src/components/common/SelectTags/index.vue')['default']
...@@ -108,6 +109,7 @@ declare global { ...@@ -108,6 +109,7 @@ declare global {
const IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default'] const IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
const IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default'] const IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
const IconTooling: typeof import('./src/components/icons/IconTooling.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 RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView'] const RouterView: typeof import('vue-router')['RouterView']
const SelectTags: typeof import('./src/components/common/SelectTags/index.vue')['default'] const SelectTags: typeof import('./src/components/common/SelectTags/index.vue')['default']
......
...@@ -42,10 +42,11 @@ interface AddOrUpdateColumnBase { ...@@ -42,10 +42,11 @@ interface AddOrUpdateColumnBase {
faceUrl?: string faceUrl?: string
imgUrl?: string imgUrl?: string
// 关联的专栏栏目 // 关联的专栏栏目
relateColumnId: number relateColumnId?: number
mainTagId: string mainTagId: string
isRelateColleague: BooleanFlag isRelateColleague: BooleanFlag
sendTime?: string sendTime?: string
isRecommend: BooleanFlag
} }
/** /**
...@@ -72,9 +73,10 @@ export interface AddOrUpdateInterviewBase { ...@@ -72,9 +73,10 @@ export interface AddOrUpdateInterviewBase {
faceUrl?: string faceUrl?: string
imgUrl?: string imgUrl?: string
// 关联的专访栏目 // 关联的专访栏目
relateColumnId: number relateColumnId?: number
mainTagId: string mainTagId: string
sendTime?: string sendTime?: string
isRecommend: BooleanFlag
} }
export interface AddOrUpdateInterviewForm extends AddOrUpdateInterviewBase { export interface AddOrUpdateInterviewForm extends AddOrUpdateInterviewBase {
......
import service from '@/utils/request/index' 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 type { BackendServicePageResult } from '@/utils/request/types'
import { ArticleTypeEnum } from '@/constants'
// 关于实践相关接口 // 关于实践相关接口
...@@ -15,3 +14,14 @@ export const addOrUpdatePractice = (data: AddOrUpdatePracticeDto) => { ...@@ -15,3 +14,14 @@ export const addOrUpdatePractice = (data: AddOrUpdatePracticeDto) => {
data, 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 { ...@@ -20,8 +20,8 @@ export interface AddOrUpdatePracticeDto {
/** /**
* 搜索文章的参数 * 搜索文章的参数
*/ */
export interface ArticleSearchParams extends PageSearchParams { export interface PracticeSearchParams extends PageSearchParams {
type?: ArticleTypeEnum sortLogic?: number
} }
/** /**
...@@ -90,3 +90,27 @@ export interface ArticleItemDto { ...@@ -90,3 +90,27 @@ export interface ArticleItemDto {
replyCount: number replyCount: number
hasPraised: BooleanFlag 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 }) => { ...@@ -24,8 +24,9 @@ export default defineComponent((_, { expose }) => {
sendType: SendTypeEnum.IMMEDIATE, sendType: SendTypeEnum.IMMEDIATE,
sendTime: '', sendTime: '',
isRelateColleague: BooleanFlag.NO, isRelateColleague: BooleanFlag.NO,
relateColumnId: 0, relateColumnId: undefined,
type: ArticleTypeEnum.COLUMN, type: ArticleTypeEnum.COLUMN,
isRecommend: BooleanFlag.NO,
}) })
const formRef = ref<InstanceType<typeof ElForm>>() const formRef = ref<InstanceType<typeof ElForm>>()
const rules = { const rules = {
...@@ -38,6 +39,7 @@ export default defineComponent((_, { expose }) => { ...@@ -38,6 +39,7 @@ export default defineComponent((_, { expose }) => {
relateColumnId: [ relateColumnId: [
{ required: true, message: '请选择专栏栏目', trigger: 'blur', type: 'number', min: 1 }, { required: true, message: '请选择专栏栏目', trigger: 'blur', type: 'number', min: 1 },
], ],
isRecommend: [{ required: true, message: '请选择是否推荐', trigger: 'blur' }],
} }
const transformForm = (releaseStatus: ReleaseStatusTypeEnum): AddOrUpdateColumnDto => { const transformForm = (releaseStatus: ReleaseStatusTypeEnum): AddOrUpdateColumnDto => {
...@@ -113,11 +115,11 @@ export default defineComponent((_, { expose }) => { ...@@ -113,11 +115,11 @@ export default defineComponent((_, { expose }) => {
<UploadFile v-model={form.value.imgUrl} /> <UploadFile v-model={form.value.imgUrl} />
</el-form-item> </el-form-item>
<el-form-item label="专栏栏目选择" prop="relateColumnId"> <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) => ( {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>
<el-form-item label="主标签" prop="mainTagId"> <el-form-item label="主标签" prop="mainTagId">
{{ {{
...@@ -126,7 +128,7 @@ export default defineComponent((_, { expose }) => { ...@@ -126,7 +128,7 @@ export default defineComponent((_, { expose }) => {
label: () => ( label: () => (
// <el-tooltip content="主标签最多选1个" placement="top"> // <el-tooltip content="主标签最多选1个" placement="top">
<span class="cursor-pointer"> <span class="cursor-pointer">
标签 标签
{/* <el-icon class="ml-1"> {/* <el-icon class="ml-1">
<InfoFilled /> <InfoFilled />
</el-icon> */} </el-icon> */}
...@@ -167,6 +169,16 @@ export default defineComponent((_, { expose }) => { ...@@ -167,6 +169,16 @@ export default defineComponent((_, { expose }) => {
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </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-form-item label="发布类型" prop="sendType">
<el-radio-group v-model={form.value.sendType} class="radio-group"> <el-radio-group v-model={form.value.sendType} class="radio-group">
<el-radio value={SendTypeEnum.IMMEDIATE} class="radio-item immediate"> <el-radio value={SendTypeEnum.IMMEDIATE} class="radio-item immediate">
...@@ -184,6 +196,12 @@ export default defineComponent((_, { expose }) => { ...@@ -184,6 +196,12 @@ export default defineComponent((_, { expose }) => {
v-model={form.value.sendTime} v-model={form.value.sendTime}
type="datetime" type="datetime"
placeholder="请选择发布时间" placeholder="请选择发布时间"
// 不能选现在
disabled-date={(time: Date) => {
return time.getTime() < Date.now() - 1000 * 60 * 60 * 24
}}
value-format="X"
style={{ width: '250px' }}
/> />
</el-form-item> </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 UploadFile from '@/components/common/UploadFile/index.vue'
import { useResetData } from '@/hooks' import { useResetData } from '@/hooks'
import { useColumnStore } from '@/stores/column' import { useInterviewStore } from '@/stores/interview'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import type { AddOrUpdateInterviewDto, AddOrUpdateInterviewForm } from '@/api/article/types' import type { AddOrUpdateInterviewDto, AddOrUpdateInterviewForm } from '@/api/article/types'
import type { TagItemDto } from '@/api/tag/types' import type { TagItemDto } from '@/api/tag/types'
export default defineComponent((_, { expose }) => { export default defineComponent((_, { expose }) => {
const columnStore = useColumnStore() const interviewStore = useInterviewStore()
const { columnList } = storeToRefs(columnStore) const { interviewList } = storeToRefs(interviewStore)
const [form, resetForm] = useResetData<AddOrUpdateInterviewForm>({ const [form, resetForm] = useResetData<AddOrUpdateInterviewForm>({
title: '', title: '',
content: '', content: '',
...@@ -22,7 +22,8 @@ export default defineComponent((_, { expose }) => { ...@@ -22,7 +22,8 @@ export default defineComponent((_, { expose }) => {
sendType: SendTypeEnum.IMMEDIATE, sendType: SendTypeEnum.IMMEDIATE,
sendTime: '', sendTime: '',
type: ArticleTypeEnum.INTERVIEW, type: ArticleTypeEnum.INTERVIEW,
relateColumnId: 0, relateColumnId: undefined,
isRecommend: BooleanFlag.NO,
}) })
const formRef = ref<InstanceType<typeof ElForm>>() const formRef = ref<InstanceType<typeof ElForm>>()
const rules = { const rules = {
...@@ -35,6 +36,7 @@ export default defineComponent((_, { expose }) => { ...@@ -35,6 +36,7 @@ export default defineComponent((_, { expose }) => {
relateColumnId: [ relateColumnId: [
{ required: true, message: '请选择专访栏目', trigger: 'blur', type: 'number', min: 1 }, { required: true, message: '请选择专访栏目', trigger: 'blur', type: 'number', min: 1 },
], ],
isRecommend: [{ required: true, message: '请选择是否推荐', trigger: 'blur' }],
} }
const transformForm = (releaseStatus: ReleaseStatusTypeEnum): AddOrUpdateInterviewDto => { const transformForm = (releaseStatus: ReleaseStatusTypeEnum): AddOrUpdateInterviewDto => {
...@@ -110,11 +112,11 @@ export default defineComponent((_, { expose }) => { ...@@ -110,11 +112,11 @@ export default defineComponent((_, { expose }) => {
<UploadFile v-model={form.value.imgUrl} /> <UploadFile v-model={form.value.imgUrl} />
</el-form-item> </el-form-item>
<el-form-item label="专访栏目选择" prop="relateColumnId"> <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) => ( {interviewList.value.map((item) => (
<el-radio value={item.id}>{item.title}</el-radio> <el-option value={item.id} label={item.title} />
))} ))}
</el-radio-group> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="主标签" prop="mainTagId"> <el-form-item label="主标签" prop="mainTagId">
{{ {{
...@@ -123,7 +125,7 @@ export default defineComponent((_, { expose }) => { ...@@ -123,7 +125,7 @@ export default defineComponent((_, { expose }) => {
label: () => ( label: () => (
// <el-tooltip content="主标签最多选1个" placement="top"> // <el-tooltip content="主标签最多选1个" placement="top">
<span class="cursor-pointer"> <span class="cursor-pointer">
标签 标签
{/* <el-icon class="ml-1"> {/* <el-icon class="ml-1">
<InfoFilled /> <InfoFilled />
</el-icon> */} </el-icon> */}
...@@ -154,6 +156,17 @@ export default defineComponent((_, { expose }) => { ...@@ -154,6 +156,17 @@ export default defineComponent((_, { expose }) => {
), ),
}} }}
</el-form-item> </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-form-item label="发布类型" prop="sendType">
<el-radio-group v-model={form.value.sendType} class="radio-group"> <el-radio-group v-model={form.value.sendType} class="radio-group">
<el-radio value={SendTypeEnum.IMMEDIATE} class="radio-item immediate"> <el-radio value={SendTypeEnum.IMMEDIATE} class="radio-item immediate">
...@@ -171,6 +184,12 @@ export default defineComponent((_, { expose }) => { ...@@ -171,6 +184,12 @@ export default defineComponent((_, { expose }) => {
v-model={form.value.sendTime} v-model={form.value.sendTime}
type="datetime" type="datetime"
placeholder="请选择发布时间" placeholder="请选择发布时间"
// 不能选现在
disabled-date={(time: Date) => {
return time.getTime() < Date.now() - 1000 * 60 * 60 * 24
}}
value-format="X"
style={{ width: '250px' }}
/> />
</el-form-item> </el-form-item>
)} )}
......
...@@ -100,7 +100,12 @@ export default defineComponent( ...@@ -100,7 +100,12 @@ export default defineComponent(
v-model={form.value.sendTime} v-model={form.value.sendTime}
type="datetime" type="datetime"
placeholder="请选择发布时间" placeholder="请选择发布时间"
// 不能选现在
disabled-date={(time: Date) => {
return time.getTime() < Date.now() - 1000 * 60 * 60 * 24
}}
value-format="X" value-format="X"
style={{ width: '250px' }}
/> />
</el-form-item> </el-form-item>
)} )}
......
...@@ -109,7 +109,7 @@ export default defineComponent((_, { expose }) => { ...@@ -109,7 +109,7 @@ export default defineComponent((_, { expose }) => {
label: () => ( label: () => (
// <el-tooltip content="主标签最多选1个" placement="top"> // <el-tooltip content="主标签最多选1个" placement="top">
<span class="cursor-pointer"> <span class="cursor-pointer">
标签 标签
{/* <el-icon class="ml-1"> {/* <el-icon class="ml-1">
<InfoFilled /> <InfoFilled />
</el-icon> */} </el-icon> */}
...@@ -157,6 +157,12 @@ export default defineComponent((_, { expose }) => { ...@@ -157,6 +157,12 @@ export default defineComponent((_, { expose }) => {
v-model={form.value.sendTime} v-model={form.value.sendTime}
type="datetime" type="datetime"
placeholder="请选择发布时间" placeholder="请选择发布时间"
// 不能选现在
disabled-date={(time: Date) => {
return time.getTime() < Date.now() - 1000 * 60 * 60 * 24
}}
value-format="X"
style={{ width: '250px' }}
/> />
</el-form-item> </el-form-item>
)} )}
......
...@@ -40,47 +40,84 @@ import type { Component } from 'vue' ...@@ -40,47 +40,84 @@ import type { Component } from 'vue'
// import { Plus } from '@element-plus/icons-vue' // import { Plus } from '@element-plus/icons-vue'
import { addOrUpdateArticle, addOrUpdatePractice } from '@/api' import { addOrUpdateArticle, addOrUpdatePractice } from '@/api'
import { ArticleTypeEnum, ReleaseStatusTypeEnum } from '@/constants' import { ArticleTypeEnum, ReleaseStatusTypeEnum } from '@/constants'
import PostForm from './postForm.tsx' import LoadingComponent from '@/components/common/LoadingComponent/index.vue'
import PracticeForm from './practiceForm.tsx'
const typeMap: Record< const typeMap: Record<
ArticleTypeEnum, ArticleTypeEnum,
{ title: string; component: Component; api?: (data: any) => Promise<any> } { title: string; component: Component; api?: (data: any) => Promise<any> }
> = { > = {
[ArticleTypeEnum.VIDEO]: { [ArticleTypeEnum.VIDEO]: {
title: '视频', title: '视频',
component: PostForm, component: defineAsyncComponent({
loader: () => import('./postForm.tsx'),
delay: 200,
loadingComponent: LoadingComponent,
}),
}, },
[ArticleTypeEnum.QUESTION]: { [ArticleTypeEnum.QUESTION]: {
title: '问题', title: '问题',
component: PostForm, component: defineAsyncComponent({
loader: () => import('./postForm.tsx'),
delay: 200,
loadingComponent: LoadingComponent,
}),
}, },
[ArticleTypeEnum.POST]: { [ArticleTypeEnum.POST]: {
title: '帖子', 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, api: addOrUpdateArticle,
}, },
[ArticleTypeEnum.PRACTICE]: { [ArticleTypeEnum.PRACTICE]: {
title: '实践', title: '实践',
component: defineAsyncComponent(() => import('./practiceForm.tsx')), component: defineAsyncComponent({
loader: () => import('./practiceForm.tsx'),
delay: 200,
loadingComponent: LoadingComponent,
}),
api: addOrUpdatePractice, api: addOrUpdatePractice,
}, },
[ArticleTypeEnum.COLUMN]: { [ArticleTypeEnum.COLUMN]: {
title: '专栏', title: '专栏',
component: defineAsyncComponent(() => import('./colnumForm.tsx')), component: defineAsyncComponent({
loader: () => import('./colnumForm.tsx'),
delay: 200,
loadingComponent: LoadingComponent,
}),
api: addOrUpdateArticle, api: addOrUpdateArticle,
}, },
[ArticleTypeEnum.INTERVIEW]: { [ArticleTypeEnum.INTERVIEW]: {
title: '专访', title: '专访',
component: defineAsyncComponent(() => import('./interviewForm.tsx')), component: defineAsyncComponent({
loader: () => import('./interviewForm.tsx'),
delay: 200,
loadingComponent: LoadingComponent,
}),
api: addOrUpdateArticle, api: addOrUpdateArticle,
}, },
} }
const dialogTitle = computed(() => '发布' + typeMap[articleType.value].title) const dialogTitle = computed(() => '发布' + typeMap[articleType.value].title)
export interface BaseFormExpose {
const formComponentRef = resetFields: () => void
useTemplateRef<InstanceType<typeof PostForm | typeof PracticeForm>>('formComponentRef') getValidatedFormData: (releaseStatus: ReleaseStatusTypeEnum) => Promise<unknown>
}
const formComponentRef = useTemplateRef<BaseFormExpose>('formComponentRef')
const currentFormComp = computed(() => { const currentFormComp = computed(() => {
return typeMap[articleType.value].component return typeMap[articleType.value].component
...@@ -100,7 +137,7 @@ const open = (type: ArticleTypeEnum) => { ...@@ -100,7 +137,7 @@ const open = (type: ArticleTypeEnum) => {
// 关闭弹窗 // 关闭弹窗
const close = () => { const close = () => {
dialogVisible.value = false dialogVisible.value = false
formComponentRef.value?.resetFields() formComponentRef?.value?.resetFields()
} }
const handleClosed = () => { const handleClosed = () => {
......
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
<!-- 搜索框 --> <!-- 搜索框 -->
<div class="flex-1 max-w-sm mx-4 hidden md:block lg:max-w-md lg:mx-6"> <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="搜索"> <el-input v-model="search" class="h-8" placeholder="搜索">
<template #prefix> <template #suffix>
<el-icon class="text-gray-400"> <el-icon class="text-gray-400" @click="router.push('/searchPage')">
<Search /> <Search />
</el-icon> </el-icon>
</template> </template>
......
...@@ -66,6 +66,12 @@ const routes = [ ...@@ -66,6 +66,12 @@ const routes = [
name: 'CulturePublishCase', name: 'CulturePublishCase',
component: () => import('@/views/publishCase/index.vue'), 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 } ...@@ -128,6 +128,7 @@ const { list, total, searchParams, loading, goToPage, changePageSize, refresh }
defaultParams: { sortLogic: 0 }, defaultParams: { sortLogic: 0 },
defaultCurrent: 1, defaultCurrent: 1,
defaultSize: 5, defaultSize: 5,
immediate: false,
}, },
) )
const tabsRef = inject(TABS_REF_KEY) const tabsRef = inject(TABS_REF_KEY)
...@@ -135,7 +136,7 @@ const tabsRef = inject(TABS_REF_KEY) ...@@ -135,7 +136,7 @@ const tabsRef = inject(TABS_REF_KEY)
const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!) const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
defineExpose({ defineExpose({
refresh: (sortLogic: number) => { refresh: (sortLogic?: number) => {
console.log('sortLogic', sortLogic) console.log('sortLogic', sortLogic)
searchParams.value.sortLogic = sortLogic searchParams.value.sortLogic = sortLogic
refresh() refresh()
......
...@@ -381,6 +381,7 @@ const { list, total, searchParams, loading, goToPage, changePageSize, refresh } ...@@ -381,6 +381,7 @@ const { list, total, searchParams, loading, goToPage, changePageSize, refresh }
defaultParams: { type: ArticleTypeEnum.VIDEO }, defaultParams: { type: ArticleTypeEnum.VIDEO },
defaultCurrent: 1, defaultCurrent: 1,
defaultSize: 12, defaultSize: 12,
immediate: false,
}, },
) )
// Tabs 配置 // Tabs 配置
...@@ -397,8 +398,7 @@ const goVideoDetail = (n: number) => { ...@@ -397,8 +398,7 @@ const goVideoDetail = (n: number) => {
router.push(`/videoDetail?id=${n}`) router.push(`/videoDetail?id=${n}`)
} }
defineExpose({ defineExpose({
refresh: (sortLogic: number) => { refresh: () => {
searchParams.value.sortLogic = sortLogic
refresh() refresh()
}, },
}) })
......
...@@ -2,11 +2,8 @@ ...@@ -2,11 +2,8 @@
<div> <div>
<div class="header h-40px items-center justify-between"> <div class="header h-40px items-center justify-between">
<div class="left flex gap-3 flex items-center"> <div class="left flex gap-3 flex items-center">
<Tabs <Tabs v-model="activeTab" :tabs="tabs" />
v-model="activeTab" <!-- 直接在@change里面拿最新的动态组件实例 拿不到 可以在@enter 动画钩子里面拿 -->
:tabs="tabs"
@change="(value) => handleTabChange(value as string)"
/>
<!-- 刷新图标 --> <!-- 刷新图标 -->
<el-icon <el-icon
size="15" size="15"
...@@ -18,8 +15,11 @@ ...@@ -18,8 +15,11 @@
</div> </div>
<el-divider style="margin: 10px 0 20px 0" /> <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> </transition>
</div> </div>
</template> </template>
...@@ -40,8 +40,9 @@ const activeTabComponent = computed(() => { ...@@ -40,8 +40,9 @@ const activeTabComponent = computed(() => {
return tabs.find((tab) => tab.value === activeTab.value)?.component return tabs.find((tab) => tab.value === activeTab.value)?.component
}) })
const activeTabComponentRef = const activeTabComponentRef = useTemplateRef<
useTemplateRef<InstanceType<typeof RecommendList>>('activeTabComponentRef') InstanceType<typeof RecommendList> | InstanceType<typeof VideoList>
>('activeTabComponentRef')
const handleRefresh = () => { const handleRefresh = () => {
if (activeTab.value === '推荐') { if (activeTab.value === '推荐') {
...@@ -49,17 +50,17 @@ const handleRefresh = () => { ...@@ -49,17 +50,17 @@ const handleRefresh = () => {
} else if (activeTab.value === '最新') { } else if (activeTab.value === '最新') {
activeTabComponentRef.value?.refresh?.(1) activeTabComponentRef.value?.refresh?.(1)
} else if (activeTab.value === '视频') { } else if (activeTab.value === '视频') {
activeTabComponentRef.value?.refresh?.(2) activeTabComponentRef.value?.refresh?.()
} }
} }
const handleTabChange = (tab: string) => { const handleEnter = () => {
if (tab === '最新') { handleRefresh()
activeTabComponentRef.value?.refresh?.(1)
} else if (tab === '推荐') {
activeTabComponentRef.value?.refresh?.(0)
}
} }
onMounted(() => {
handleRefresh()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
...@@ -17,8 +17,7 @@ ...@@ -17,8 +17,7 @@
{{ item.title }} {{ item.title }}
</h3> </h3>
<div class="flex items-center cursor-pointer"> <div class="flex items-center cursor-pointer">
<span class="mr-1 text-14px color-#606266">查看更多</span> <span class="mr-1 text-14px color-#606266">查看更多 >></span>
<el-icon><ArrowRight /></el-icon>
</div> </div>
</div> </div>
...@@ -35,10 +34,12 @@ ...@@ -35,10 +34,12 @@
:src="i.faceUrl" :src="i.faceUrl"
class="w-full aspect-[5/3] 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"> <div
<el-tag size="small" class="bg-orange-500 text-white border-none"> v-if="i.isRecommend"
1111111 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"
</el-tag> >
<img class="w-6" src="@/assets/img/culture/recommend.png" alt="" />
<div class="text-12px text-#000 line-height-12px">推荐</div>
</div> </div>
</div> </div>
<h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors"> <h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors">
......
...@@ -18,9 +18,8 @@ ...@@ -18,9 +18,8 @@
</h3> </h3>
<div class="flex items-center cursor-pointer hover:text-[var(--dynamic-color)]"> <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 class="mr-1 text-14px color-#606266 hover:text-[var(--dynamic-color)]"
>查看更多</span >查看更多 >></span
> >
<el-icon><ArrowRight /></el-icon>
</div> </div>
</div> </div>
...@@ -35,12 +34,14 @@ ...@@ -35,12 +34,14 @@
<div class="relative mb-3 overflow-hidden rounded-lg"> <div class="relative mb-3 overflow-hidden rounded-lg">
<img <img
:src="i.faceUrl" :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"> <div
<el-tag size="small" class="bg-orange-500 text-white border-none"> v-if="i.isRecommend"
1111111 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"
</el-tag> >
<img class="w-6" src="@/assets/img/culture/recommend.png" alt="" />
<div class="text-12px text-#000 line-height-12px">推荐</div>
</div> </div>
</div> </div>
<h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors"> <h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors">
......
...@@ -143,7 +143,13 @@ ...@@ -143,7 +143,13 @@
type="datetime" type="datetime"
placeholder="选择发布时间" placeholder="选择发布时间"
size="small" 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 <el-switch
v-model="form.sendType" 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({ ...@@ -50,8 +50,8 @@ export default defineConfig({
port: 3000, port: 3000,
proxy: { proxy: {
'/api1': { '/api1': {
// target: 'http://192.168.2.168:8089', // 立鹏本地 target: 'http://192.168.2.168:8089', // 立鹏本地
target: 'http://192.168.2.55:8089', // 首拥本地 // target: 'http://192.168.2.55:8089', // 首拥本地
changeOrigin: true, changeOrigin: true,
rewrite: (path) => { rewrite: (path) => {
return path.replace(/^\/api1/, '') 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