Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
corporateCulture-qd
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
王立鹏
corporateCulture-qd
Commits
14475b0a
Commit
14475b0a
authored
Nov 18, 2025
by
lijiabin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
【需求 17679】 wip: 继续完善首页部分
parent
b3cb242d
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
487 additions
and
225 deletions
+487
-225
types.ts
src/api/article/types.ts
+3
-3
index.ts
src/api/index.ts
+6
-0
index.ts
src/api/practice/index.ts
+17
-0
types.ts
src/api/practice/types.ts
+92
-0
index.tsx
src/components/common/Tabs/index.tsx
+10
-7
index.vue
src/components/common/UploadFile/index.vue
+129
-61
enums.ts
src/constants/enums.ts
+1
-1
index.ts
src/hooks/index.ts
+1
-0
useHintAnimation.ts
src/hooks/useHintAnimation.ts
+30
-0
postForm.tsx
src/layoutCulture/components/postForm.tsx
+11
-6
practiceForm.tsx
src/layoutCulture/components/practiceForm.tsx
+17
-14
publishDialog.vue
src/layoutCulture/components/publishDialog.vue
+24
-12
recommendList.vue
src/views/homePage/homeTab/components/recommendList.vue
+5
-93
index.vue
src/views/homePage/homeTab/index.vue
+30
-7
index.vue
src/views/homePage/index.vue
+20
-4
practiceList.vue
src/views/homePage/yaTab/components/practiceList.vue
+91
-17
No files found.
src/api/article/types.ts
View file @
14475b0a
import
{
ArticleTypeEnum
,
ReleaseStatusEnum
,
BooleanFlag
}
from
'@/constants'
import
{
ArticleTypeEnum
,
ReleaseStatus
Type
Enum
,
BooleanFlag
}
from
'@/constants'
import
type
{
PageSearchParams
}
from
'@/utils/request/types'
/**
...
...
@@ -34,7 +34,7 @@ export interface AddOrUpdateArticleDto {
mainTagId
?:
number
/** 发布状态 */
releaseStatus
?:
ReleaseStatusEnum
releaseStatus
?:
ReleaseStatus
Type
Enum
/** 标签列表 */
tagList
?:
{
tagId
:
number
;
sort
:
number
}[]
...
...
@@ -67,7 +67,7 @@ export interface ArticleItemDto {
isRecommend
:
BooleanFlag
type
:
ArticleTypeEnum
isRelateColleague
:
BooleanFlag
releaseStatus
:
ReleaseStatusEnum
releaseStatus
:
ReleaseStatus
Type
Enum
tagNameList
:
string
[]
praiseCount
:
number
collectionCount
:
number
...
...
src/api/index.ts
View file @
14475b0a
...
...
@@ -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'
src/api/practice/index.ts
0 → 100644
View file @
14475b0a
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
,
})
}
src/api/practice/types.ts
0 → 100644
View file @
14475b0a
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
}
src/components/common/Tabs/index.tsx
View file @
14475b0a
import
type
{
SetupContext
}
from
'vue'
type
TypeOfValue
=
string
|
number
type
TabsProps
=
{
interface
TabsProps
<
T
>
{
tabs
:
{
label
:
string
value
:
T
ypeOfValue
value
:
T
}[]
modelValue
:
T
ypeOfValue
modelValue
:
T
}
type
TabsEmits
=
{
'update:modelValue'
:
[
T
ypeOfValue
]
change
:
[
T
ypeOfValue
]
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
}
...
...
src/components/common/UploadFile/index.vue
View file @
14475b0a
...
...
@@ -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
>
src/constants/enums.ts
View file @
14475b0a
...
...
@@ -9,7 +9,7 @@ export enum ArticleTypeEnum {
}
// 发布状态枚举
export
enum
ReleaseStatusEnum
{
export
enum
ReleaseStatus
Type
Enum
{
DRAFT
=
1
,
// 草稿
PUBLISH
=
2
,
// 发布
}
...
...
src/hooks/index.ts
View file @
14475b0a
export
*
from
'./useResetData'
export
*
from
'./usePageSearch'
export
*
from
'./useScrollTop'
export
*
from
'./useHintAnimation'
src/hooks/useHintAnimation.ts
0 → 100644
View file @
14475b0a
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
}
}
src/layoutCulture/components/postForm.tsx
View file @
14475b0a
import
{
ArticleTypeEnum
,
ReleaseStatusEnum
}
from
'@/constants'
import
{
ArticleTypeEnum
,
ReleaseStatus
Type
Enum
}
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
:
ReleaseStatus
Enum
.
PUBLISH
,
releaseStatus
:
ReleaseStatusType
Enum
.
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=
{
ReleaseStatus
Enum
.
PUBLISH
}
class=
"radio-item immediate"
>
<
el
-
radio
value=
{
ReleaseStatusType
Enum
.
PUBLISH
}
class=
"radio-item immediate"
>
立即发布
</
el
-
radio
>
<
el
-
radio
value=
{
ReleaseStatus
Enum
.
DRAFT
}
class=
"radio-item scheduled"
>
<
el
-
radio
value=
{
ReleaseStatusType
Enum
.
DRAFT
}
class=
"radio-item scheduled"
>
定时发布
</
el
-
radio
>
</
el
-
radio
-
group
>
...
...
@@ -83,4 +84,8 @@ export default defineComponent((_, { expose }) => {
</
el
-
form
>
</
div
>
)
})
},
{
name
:
'PostForm'
,
},
)
src/layoutCulture/components/practiceForm.tsx
View file @
14475b0a
import
{
ArticleTypeEnum
,
ReleaseStatus
Enum
,
SendTypeEnum
}
from
'@/constants'
import
{
ReleaseStatusType
Enum
,
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=
"
face
Url"
>
<
el
-
form
-
item
label=
"图片"
prop=
"
img
Url"
>
{
/* @ts-ignore */
}
<
UploadFile
v
-
model=
{
form
.
value
.
face
Url
}
/>
<
UploadFile
v
-
model=
{
form
.
value
.
img
Url
}
/>
</
el
-
form
-
item
>
<
el
-
form
-
item
label=
"主标签"
prop=
"mainTagId"
>
{
{
...
...
src/layoutCulture/components/publishDialog.vue
View file @
14475b0a
...
...
@@ -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
handleS
aveDraft
=
async
()
=>
{}
const
handleSubmit
=
async
()
=>
{
const
formData
=
await
formComponentRef
.
value
?.
validate
(
)
const
handleS
ubmit
=
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
}
}
// 暴露方法给父组件
...
...
src/views/homePage/homeTab/components/recommendList.vue
View file @
14475b0a
...
...
@@ -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
>
src/views/homePage/homeTab/index.vue
View file @
14475b0a
...
...
@@ -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
>
...
...
src/views/homePage/index.vue
View file @
14475b0a
...
...
@@ -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
=
()
=>
{
...
...
src/views/homePage/yaTab/components/practiceList.vue
View file @
14475b0a
...
...
@@ -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
},
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment