Commit 02392d7f by lijiabin

【需求 17679】 feat: 完成【问吧】页面

parent 5b4d9245
...@@ -12,13 +12,16 @@ export {} ...@@ -12,13 +12,16 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ArticleContent: typeof import('./src/components/common/ArticleContent/index.vue')['default']
BaseButton: typeof import('./src/components/common/ElComponents/ElButton/BaseButton.vue')['default'] BaseButton: typeof import('./src/components/common/ElComponents/ElButton/BaseButton.vue')['default']
Comment: typeof import('./src/components/common/Comment/index.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButto: typeof import('element-plus/es')['ElButto']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCarousel: typeof import('element-plus/es')['ElCarousel'] ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem'] ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
...@@ -33,6 +36,9 @@ declare module 'vue' { ...@@ -33,6 +36,9 @@ declare module 'vue' {
ElImage: typeof import('element-plus/es')['ElImage'] ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption'] ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination'] ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover'] ElPopover: typeof import('element-plus/es')['ElPopover']
...@@ -40,6 +46,7 @@ declare module 'vue' { ...@@ -40,6 +46,7 @@ declare module 'vue' {
ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
...@@ -54,9 +61,11 @@ declare module 'vue' { ...@@ -54,9 +61,11 @@ declare module 'vue' {
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'] LoadingComponent: typeof import('./src/components/common/LoadingComponent/index.vue')['default']
PublishBox: typeof import('./src/components/common/PublishBox/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']
SelectTagsDialog: typeof import('./src/components/common/PublishBox/components/selectTagsDialog.vue')['default']
SvgIcon: typeof import('./src/components/common/SvgIcon/svgIcon.vue')['default'] SvgIcon: typeof import('./src/components/common/SvgIcon/svgIcon.vue')['default']
UploadFile: typeof import('./src/components/common/UploadFile/index.vue')['default'] UploadFile: typeof import('./src/components/common/UploadFile/index.vue')['default']
UploadVideo: typeof import('./src/components/common/UploadVideo/index.vue')['default'] UploadVideo: typeof import('./src/components/common/UploadVideo/index.vue')['default']
...@@ -68,13 +77,16 @@ declare module 'vue' { ...@@ -68,13 +77,16 @@ declare module 'vue' {
// For TSX support // For TSX support
declare global { declare global {
const ArticleContent: typeof import('./src/components/common/ArticleContent/index.vue')['default']
const BaseButton: typeof import('./src/components/common/ElComponents/ElButton/BaseButton.vue')['default'] const BaseButton: typeof import('./src/components/common/ElComponents/ElButton/BaseButton.vue')['default']
const Comment: typeof import('./src/components/common/Comment/index.vue')['default']
const ElAlert: typeof import('element-plus/es')['ElAlert']
const ElAvatar: typeof import('element-plus/es')['ElAvatar'] const ElAvatar: typeof import('element-plus/es')['ElAvatar']
const ElButto: typeof import('element-plus/es')['ElButto']
const ElButton: typeof import('element-plus/es')['ElButton'] const ElButton: typeof import('element-plus/es')['ElButton']
const ElCard: typeof import('element-plus/es')['ElCard'] const ElCard: typeof import('element-plus/es')['ElCard']
const ElCarousel: typeof import('element-plus/es')['ElCarousel'] const ElCarousel: typeof import('element-plus/es')['ElCarousel']
const ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem'] const ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
const ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
const ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] const ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
const ElDialog: typeof import('element-plus/es')['ElDialog'] const ElDialog: typeof import('element-plus/es')['ElDialog']
...@@ -89,6 +101,9 @@ declare global { ...@@ -89,6 +101,9 @@ declare global {
const ElImage: typeof import('element-plus/es')['ElImage'] const ElImage: typeof import('element-plus/es')['ElImage']
const ElInput: typeof import('element-plus/es')['ElInput'] const ElInput: typeof import('element-plus/es')['ElInput']
const ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] const ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
const ElLink: typeof import('element-plus/es')['ElLink']
const ElMenu: typeof import('element-plus/es')['ElMenu']
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
const ElOption: typeof import('element-plus/es')['ElOption'] const ElOption: typeof import('element-plus/es')['ElOption']
const ElPagination: typeof import('element-plus/es')['ElPagination'] const ElPagination: typeof import('element-plus/es')['ElPagination']
const ElPopover: typeof import('element-plus/es')['ElPopover'] const ElPopover: typeof import('element-plus/es')['ElPopover']
...@@ -96,6 +111,7 @@ declare global { ...@@ -96,6 +111,7 @@ declare global {
const ElRadio: typeof import('element-plus/es')['ElRadio'] const ElRadio: typeof import('element-plus/es')['ElRadio']
const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
const ElSelect: typeof import('element-plus/es')['ElSelect'] const ElSelect: typeof import('element-plus/es')['ElSelect']
const ElSlider: typeof import('element-plus/es')['ElSlider']
const ElSwitch: typeof import('element-plus/es')['ElSwitch'] const ElSwitch: typeof import('element-plus/es')['ElSwitch']
const ElTable: typeof import('element-plus/es')['ElTable'] const ElTable: typeof import('element-plus/es')['ElTable']
const ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] const ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
...@@ -110,9 +126,11 @@ declare global { ...@@ -110,9 +126,11 @@ declare global {
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 LoadingComponent: typeof import('./src/components/common/LoadingComponent/index.vue')['default']
const PublishBox: typeof import('./src/components/common/PublishBox/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']
const SelectTagsDialog: typeof import('./src/components/common/PublishBox/components/selectTagsDialog.vue')['default']
const SvgIcon: typeof import('./src/components/common/SvgIcon/svgIcon.vue')['default'] const SvgIcon: typeof import('./src/components/common/SvgIcon/svgIcon.vue')['default']
const UploadFile: typeof import('./src/components/common/UploadFile/index.vue')['default'] const UploadFile: typeof import('./src/components/common/UploadFile/index.vue')['default']
const UploadVideo: typeof import('./src/components/common/UploadVideo/index.vue')['default'] const UploadVideo: typeof import('./src/components/common/UploadVideo/index.vue')['default']
......
<template> <template>
<h1>ccccccccccccccccccccccccccccccccccccccccccccccccccccc1</h1>
<h1>ccccccccccccccccccccccccccccccccccccccccccccccccccccc1</h1>
<h1>ccccccccccccccccccccccccccccccccccccccccccccccccccccc1</h1>
<h1>ccccccccccccccccccccccccccccccccccccccccccccccccccccc1</h1>
<h1>ccccccccccccccccccccccccccccccccccccccccccccccccccccc1</h1>
<h1>ccccccccccccccccccccccccccccccccccccccccccccccccccccc1</h1>
<h1>ccccccccccccccccccccccccccccccccccccccccccccccccccccc1</h1>
<el-config-provider :locale="locale"> <el-config-provider :locale="locale">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<component :is="Component" /> <component :is="Component" />
...@@ -9,8 +16,8 @@ ...@@ -9,8 +16,8 @@
import zhCn from 'element-plus/es/locale/lang/zh-cn' import zhCn from 'element-plus/es/locale/lang/zh-cn'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
const locale = ref(zhCn) const locale = ref(zhCn)
const userStore = useUserStore() // const userStore = useUserStore()
userStore.fetchUserInfo().then((res) => { // userStore.fetchUserInfo().then((res) => {
console.log(res) // console.log(res)
}) // })
</script> </script>
...@@ -94,6 +94,21 @@ export const getColumnList = (data: PageSearchParams) => { ...@@ -94,6 +94,21 @@ export const getColumnList = (data: PageSearchParams) => {
}, },
}) })
} }
/**
* 获取首页视频列表list —— 分页
*/
export const getVideoList = (data: PageSearchParams) => {
return service.request<BackendServicePageResult<ColumnItemDto>>({
url: '/api/yaCulture/listByPage',
method: 'POST',
data: {
...data,
type: 'video',
},
})
}
/** /**
* 点赞或者取消点赞文章 * 点赞或者取消点赞文章
*/ */
...@@ -179,3 +194,15 @@ export const deleteComment = (commentId: number) => { ...@@ -179,3 +194,15 @@ export const deleteComment = (commentId: number) => {
method: 'POST', method: 'POST',
}) })
} }
/**
* 给文章投币
*/
export const addOrCancelArticleReward = (data: { articleId: number; ayabi: number }) => {
return service.request({
url: `/api/culture/action/record/reward`,
method: 'POST',
data,
})
}
...@@ -143,6 +143,7 @@ export interface ArticleItemDto { ...@@ -143,6 +143,7 @@ export interface ArticleItemDto {
showAvatar: string showAvatar: string
showName: string showName: string
videoDuration: string videoDuration: string
showComment?: boolean
} }
/** /**
...@@ -259,5 +260,6 @@ export interface CommentItemDto { ...@@ -259,5 +260,6 @@ export interface CommentItemDto {
region: string region: string
regionHide: number regionHide: number
replyUser: string replyUser: string
replyName: string
userId: number userId: number
} }
...@@ -12,7 +12,7 @@ export * from './case' ...@@ -12,7 +12,7 @@ export * from './case'
export * from './home' export * from './home'
export * from './practice' export * from './practice'
export * from './common' export * from './common'
export * from './login'
export * from './article' export * from './article'
// 导出类型 // 导出类型
export * from './task/types' export * from './task/types'
...@@ -27,5 +27,5 @@ export * from './case/types' ...@@ -27,5 +27,5 @@ export * from './case/types'
export * from './home/types' export * from './home/types'
export * from './practice/types' export * from './practice/types'
export * from './common/types' export * from './common/types'
export * from './login/types'
export * from './article/types' export * from './article/types'
...@@ -2,7 +2,7 @@ import service from '@/utils/request/index' ...@@ -2,7 +2,7 @@ import service from '@/utils/request/index'
import type { LoginParams, LoginResponseDto } from './types' import type { LoginParams, LoginResponseDto } from './types'
/** /**
* 登录 * 登录 —— 根据 邮箱密码登录
*/ */
export const loginByEmail = (data: LoginParams) => { export const loginByEmail = (data: LoginParams) => {
return service.request<LoginResponseDto>({ return service.request<LoginResponseDto>({
...@@ -11,3 +11,45 @@ export const loginByEmail = (data: LoginParams) => { ...@@ -11,3 +11,45 @@ export const loginByEmail = (data: LoginParams) => {
data, data,
}) })
} }
/**
* 企业微信应用登录
* 1. 直接拿 code登录
* 2. 根据 code + cutEmail + isCodeLogin // 切换账号登录
*/
export const loginByCode = ({
code,
isCodeLogin,
cutEmail,
}: {
code: string
isCodeLogin?: number
cutEmail?: string
}) => {
return service.request<LoginResponseDto>({
url: '/api/auth/applicationLogin',
method: 'POST',
data: {
code,
isCodeLogin,
cutEmail,
},
})
}
/**
* 生成随机密钥-切换官方账号用
*/
interface GenerateLoginKeyData {
cutEmail: string
timestamp: number
type: 1 | 2 // 1: 多平台跳转 2: 官方账号切换
userId: number
}
export const generateLoginKey = (data: GenerateLoginKeyData) => {
return service.request<string>({
url: '/api/auth/generateLoginKey',
method: 'POST',
data,
})
}
...@@ -24,4 +24,5 @@ export interface LoginResponseDto { ...@@ -24,4 +24,5 @@ export interface LoginResponseDto {
accountNonExpired: boolean accountNonExpired: boolean
credentialsNonExpired: boolean credentialsNonExpired: boolean
accountNonLocked: boolean accountNonLocked: boolean
token: string
} }
...@@ -15,6 +15,7 @@ export interface AddOrUpdatePracticeDto { ...@@ -15,6 +15,7 @@ export interface AddOrUpdatePracticeDto {
tagList: { tagId: number; sort: number }[] tagList: { tagId: number; sort: number }[]
sendType: SendTypeEnum sendType: SendTypeEnum
sendTime: string sendTime: string
type?: ArticleTypeEnum
} }
/** /**
......
...@@ -127,4 +127,5 @@ export interface AuditListItemDto { ...@@ -127,4 +127,5 @@ export interface AuditListItemDto {
export interface AuditArticleDto { export interface AuditArticleDto {
articleId: number articleId: number
auditResult: Exclude<AuditStatusEnum, AuditStatusEnum.UNAUDITED> auditResult: Exclude<AuditStatusEnum, AuditStatusEnum.UNAUDITED>
auditRemark?: string
} }
<template> <template>
<div class="min-h-screen px-20"> <div
<!-- 主内容区 --> class="bg-white backdrop-blur-sm rounded-lg shadow-sm border border-white/50 overflow-hidden"
<ActionButtons >
v-model="articleDetail" <!-- 发布者信息 -->
@scrollToCommentBox="commentRef?.scrollToCommentBox()" <div class="p-6 border-b border-gray-100">
></ActionButtons> <div class="flex items-center gap-4">
<div class="lg:col-span-3"> <div class="relative">
<!-- 帖子主体 --> <img
<div :src="articleDetail?.createUserAvatar"
class="bg-white backdrop-blur-sm rounded-lg shadow-sm border border-white/50 overflow-hidden" alt=""
> class="w-12 h-12 rounded-full object-cover"
<!-- 发布者信息 --> />
<div class="p-6 border-b border-gray-100"> <div
<div class="flex items-center gap-4"> class="absolute -bottom-1 -right-1 w-6 h-6 bg-gradient-to-r from-yellow-400 to-orange-400 rounded-full flex items-center justify-center text-xs font-bold text-white"
<div class="relative"> >
<img 8
:src="articleDetail?.createUserAvatar"
alt=""
class="w-12 h-12 rounded-full object-cover"
/>
<div
class="absolute -bottom-1 -right-1 w-6 h-6 bg-gradient-to-r from-yellow-400 to-orange-400 rounded-full flex items-center justify-center text-xs font-bold text-white"
>
8
</div>
</div>
<div class="flex-1">
<div class="flex items-center gap-2">
<h3 class="font-semibold text-gray-800">{{ articleDetail?.createUserName }}</h3>
<span
class="px-2 py-0.5 text-xs bg-gradient-to-r from-purple-100 to-blue-100 text-purple-600 rounded-full"
>
资深前端工程师
</span>
</div>
<p class="text-sm text-gray-500 mt-1">
{{ dayjs((articleDetail?.createTime || 0) * 1000).format('YYYY-MM-DD HH:mm:ss') }}
· {{ articleDetail?.viewCount || 0 }} 阅读
</p>
</div>
<!-- <button
class="px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all"
>
+ 关注
</button> -->
</div> </div>
</div> </div>
<div class="flex-1">
<!-- 帖子内容 --> <div class="flex items-center gap-2">
<div class="p-6"> <h3 class="font-semibold text-gray-800">{{ articleDetail?.createUserName }}</h3>
<h1 class="text-2xl font-bold text-gray-900 mb-4 leading-tight">
{{ articleDetail?.title }}
</h1>
<!-- 文章内容 -->
<div class="prose prose-lg max-w-none">
<div class="text-gray-700 leading-relaxed space-y-4">
{{ articleDetail?.content }}
</div>
<!-- 图片内容 -->
<div v-if="articleDetail.imgUrl" class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
<el-image
v-for="item in articleDetail.imgUrl.split(',')"
:key="item"
:src="item"
fit="cover"
class="rounded-lg w-full h-64 hover:scale-105 transition-transform cursor-pointer"
:preview-src-list="articleDetail.imgUrl.split(',')"
:preview-teleported="true"
/>
</div>
</div>
<!-- 标签 -->
<div class="flex flex-wrap gap-2 mt-6">
<span <span
v-for="item in articleDetail?.tagNameList" class="px-2 py-0.5 text-xs bg-gradient-to-r from-purple-100 to-blue-100 text-purple-600 rounded-full"
:key="item"
class="px-3 py-1 text-sm bg-gradient-to-r from-cyan-100 to-blue-100 text-cyan-600 rounded-full hover:shadow-md transition-all cursor-pointer"
> >
#{{ item }} 资深前端工程师
</span> </span>
</div> </div>
<p class="text-sm text-gray-500 mt-1">
{{ dayjs((articleDetail?.createTime || 0) * 1000).format('YYYY-MM-DD HH:mm:ss') }}
· {{ articleDetail?.viewCount || 0 }} 阅读
</p>
</div>
<el-button type="primary" link>{{ articleType }}</el-button>
</div>
</div>
<!-- 帖子内容 -->
<div class="p-6">
<h1 class="text-2xl font-bold text-gray-900 mb-4 leading-tight">
{{ articleDetail?.title }}
</h1>
<!-- 文章内容 -->
<div class="prose prose-lg max-w-none">
<div class="text-gray-700 leading-relaxed space-y-4">
{{ articleDetail?.content }}
</div>
<!-- 图片内容 -->
<div
v-if="articleDetail.imgUrl && articleDetail.type !== ArticleTypeEnum.VIDEO"
class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6"
>
<el-image
v-for="item in articleDetail.imgUrl.split(',')"
:key="item"
:src="item"
fit="cover"
class="rounded-lg w-full h-64 hover:scale-105 transition-transform cursor-pointer"
:preview-src-list="articleDetail.imgUrl.split(',')"
:preview-teleported="true"
/>
</div>
<div v-if="articleDetail.type === ArticleTypeEnum.VIDEO">
<video :src="articleDetail.videoUrl" controls />
</div> </div>
</div> </div>
<!-- 评论区 --> <!-- 标签 -->
<Comment ref="commentRef" :id="id" v-model:total="articleDetail.replyCount" /> <div class="flex flex-wrap gap-2 mt-6">
<span
v-for="item in articleDetail?.tagNameList"
:key="item"
class="px-3 py-1 text-sm bg-gradient-to-r from-cyan-100 to-blue-100 text-cyan-600 rounded-full hover:shadow-md transition-all cursor-pointer"
>
#{{ item }}
</span>
</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { getArticleDetail, type ArticleItemDto } from '@/api' import { type ArticleItemDto } from '@/api'
import ActionButtons from './components/actionButtons.vue' import { articleTypeListOptions, ArticleTypeEnum } from '@/constants'
import Comment from '@/components/common/Comment/index.vue'
const commentRef = useTemplateRef<typeof Comment | null>('commentRef')
const route = useRoute()
const id = route.params.articleId as string
const articleDetail = ref({} as ArticleItemDto)
const initPage = () => { const { articleDetail } = defineProps<{
Promise.allSettled([getArticleDetail(id)]).then(([r1]) => { articleDetail: ArticleItemDto
if (r1.status === 'fulfilled') { }>()
articleDetail.value = r1.value.data const articleType = computed(() => {
} return articleTypeListOptions.find((item) => item.value === articleDetail.type)?.label
})
}
onMounted(async () => {
initPage()
}) })
</script> </script>
...@@ -9,35 +9,48 @@ ...@@ -9,35 +9,48 @@
<span class="text-lg font-semibold text-gray-800">评论 ({{ total }})</span> <span class="text-lg font-semibold text-gray-800">评论 ({{ total }})</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
class="cursor-pointer px-3 py-1.5 text-sm bg-gradient-to-r text-gray-600 rounded-full transition-all hover:bg-gray-100" class="cursor-pointer px-3 py-1.5 text-sm transition-all relative"
@click="((searchParams.sortType = 2), refresh())" @click="((searchParams.sortType = 2), refresh())"
:class="{ :class="{
'bg-gradient-to-r from-blue-500 to-purple-500 text-white! shadow-md ': 'text-indigo-600 font-medium': searchParams.sortType === 2,
searchParams.sortType === 2, 'text-gray-600 hover:text-gray-900': searchParams.sortType !== 2,
}" }"
> >
最新 最新
<span
v-if="searchParams.sortType === 2"
class="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-600"
></span>
</button> </button>
<button <button
class="cursor-pointer px-3 py-1.5 text-sm bg-gradient-to-r text-gray-600 rounded-full transition-all hover:bg-gray-100" class="cursor-pointer px-3 py-1.5 text-sm transition-all relative"
@click="((searchParams.sortType = 1), refresh())"
:class="{ :class="{
'bg-gradient-to-r from-blue-500 to-purple-500 text-white! shadow-md': 'text-indigo-600 font-medium': searchParams.sortType === 1,
searchParams.sortType === 1, 'text-gray-600 hover:text-gray-900': searchParams.sortType !== 1,
}" }"
@click="((searchParams.sortType = 1), refresh())"
> >
最多评论 最多评论
<span
v-if="searchParams.sortType === 1"
class="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-600"
></span>
</button> </button>
<button <button
class="cursor-pointer px-3 py-1.5 text-sm bg-gradient-to-r text-gray-600 rounded-full transition-all hover:bg-gray-100" class="cursor-pointer px-3 py-1.5 text-sm transition-all relative"
@click="((searchParams.sortType = 4), refresh())"
:class="{ :class="{
'bg-gradient-to-r from-blue-500 to-purple-500 text-white! shadow-md': 'text-indigo-600 font-medium': searchParams.sortType === 4,
searchParams.sortType === 4, 'text-gray-600 hover:text-gray-900': searchParams.sortType !== 4,
}" }"
@click="((searchParams.sortType = 4), refresh())"
> >
最多点赞 最多点赞
<span
v-if="searchParams.sortType === 4"
class="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-600"
></span>
</button> </button>
<!-- <Tabs v-model="searchParams.sortType" :tabs="tabs" /> --> <!-- <Tabs v-model="searchParams.sortType" :tabs="tabs" /> -->
</div> </div>
</div> </div>
...@@ -133,9 +146,11 @@ ...@@ -133,9 +146,11 @@
<img :src="child.avatar" alt="" class="w-8 h-8 rounded-full object-cover" /> <img :src="child.avatar" alt="" class="w-8 h-8 rounded-full object-cover" />
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-2 mb-1"> <div class="flex items-center gap-2 mb-1">
<span class="font-medium text-sm text-gray-800">{{ child.replyUser }}</span> <span class="font-medium text-sm text-gray-800"
>{{ child.replyUser }} 回复 @{{ child.replyName }}</span
>
</div> </div>
<p class="text-sm text-gray-700"> <p class="text-gray-700">
{{ child.content }} {{ child.content }}
</p> </p>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
...@@ -228,8 +243,9 @@ import dayjs from 'dayjs' ...@@ -228,8 +243,9 @@ import dayjs from 'dayjs'
import { useUserStore } from '@/stores' import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
const { id } = defineProps<{ const { id, defaultSize = 10 } = defineProps<{
id: number | string id: number | string
defaultSize?: number
}>() }>()
const total = defineModel<number>('total', { required: true, default: 0 }) const total = defineModel<number>('total', { required: true, default: 0 })
...@@ -253,6 +269,7 @@ const { list, searchParams, goToPage, loading, changePageSize, refresh } = usePa ...@@ -253,6 +269,7 @@ const { list, searchParams, goToPage, loading, changePageSize, refresh } = usePa
articleId: id, articleId: id,
sortType: 2, sortType: 2,
}, },
defaultSize,
}, },
) )
const handleCurrentChange = async (e: number) => { const handleCurrentChange = async (e: number) => {
......
<template>
<el-dialog v-model="dialogVisible" title="选择标签" width="500px" :close-on-click-modal="false">
<div class="space-y-6 px-2">
<div class="flex items-start gap-4">
<div class="text-sm text-gray-700 w-16 flex-shrink-0">主标签</div>
<div class="flex-1">
<SelectTags v-model="mainTagId" />
</div>
</div>
<div class="flex items-start gap-4">
<div class="text-sm text-gray-700 w-16 flex-shrink-0">副标签</div>
<div class="flex-1">
<SelectTags
v-model="subTagIdList"
:max-selected-tags="3"
:filter-tags-fn="filterTagsFn"
/>
</div>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import SelectTags from '@/components/common/SelectTags/index.vue'
import type { TagItemDto } from '@/api'
const dialogVisible = ref(false)
const mainTagId = defineModel<string>('mainTagId', { required: true })
const subTagIdList = defineModel<number[]>('tagList', { required: true })
const open = () => {
dialogVisible.value = true
}
const filterTagsFn = (allTags: TagItemDto[]) => {
return allTags.filter((tag) => tag.id !== Number(mainTagId.value))
}
defineExpose({
open,
})
</script>
<style scoped></style>
<template>
<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 items-start">
<!-- 用户头像 -->
<el-avatar :size="48" :src="userInfo.avatar" class="flex-shrink-0">
<el-icon><User /></el-icon>
</el-avatar>
<!-- 输入区域 -->
<div class="flex-1">
<!-- 话题标签输入 -->
<div class="mb-4">
<el-input
v-model="form.title"
:placeholder="textMap[type].title"
class="tag-input"
clearable
/>
</div>
<!-- 主要内容输入 -->
<div class="relative mb-3">
<el-input
type="textarea"
:placeholder="textMap[type].content"
:rows="3"
:maxlength="500"
resize="none"
class="main-textarea"
v-model="form.content"
/>
<!-- 字符计数 -->
<div class="absolute bottom-3 right-3 text-xs text-gray-400">1/30</div>
</div>
<!-- 标签内容 -->
<div class="mb-2">
<!-- 选择的标签内容 -->
<div class="flex items-center gap-2">
<span v-if="mainTagText" class="text-sm text-gray-500"
>主标签:
<el-tag>{{ mainTagText }}</el-tag>
</span>
<span v-if="subTagTextList.length > 0" class="text-sm text-gray-500"
>副标签:
<el-tag class="mr-2" v-for="tag in subTagTextList" :key="tag">{{ tag }}</el-tag>
</span>
</div>
</div>
<!-- 图片相关 -->
<div v-if="form.imgUrl.length" class="flex flex-wrap gap-2">
<!-- 删除图片 -->
<div
class="relative w-20 h-20 rounded-lg overflow-hidden group"
v-for="img in form.imgUrl"
:key="img"
>
<div
class="absolute top-1 right-1 z-10 w-5 h-5 flex items-center justify-center bg-black/60 rounded-full cursor-pointer opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-black/80 hover:scale-110"
@click="handleDeleteImg(img)"
>
<el-icon class="text-white text-xs">
<Close />
</el-icon>
</div>
<el-image
:src="img"
class="w-full h-full rounded-lg border border-gray-200"
fit="cover"
/>
</div>
</div>
</div>
</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"
@click="handleAddTag"
>
<el-icon size="18"><CollectionTag /></el-icon>
</el-button>
</el-tooltip>
<!-- 隐藏上传文件的input -->
<input type="file" class="hidden" ref="fileInputRef" @change="handleFileChange" />
<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"
@click="fileInputRef?.click()"
>
<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="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"
@click="handlePublish(ReleaseStatusTypeEnum.DRAFT)"
>
存草稿
</el-button>
<el-button
type="primary"
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"
@click="handlePublish(ReleaseStatusTypeEnum.PUBLISH)"
>
发布 {{ type === ArticleTypeEnum.QUESTION ? '问题' : '实践' }}
</el-button>
</div>
</div>
</div>
<SelectTagsDialog
v-model:mainTagId="form.mainTagId"
v-model:tagList="form.tagList"
ref="selectTagsDialogRef"
/>
</div>
</template>
<!-- 发布实践 或者 问吧 的 发布框 -->
<script setup lang="ts">
import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
import SelectTagsDialog from './components/selectTagsDialog.vue'
import { useResetData } from '@/hooks'
import { ArticleTypeEnum, ReleaseStatusTypeEnum, SendTypeEnum } from '@/constants'
import { useTagsStore } from '@/stores'
import { uploadFile } from '@/api'
import { Close } from '@element-plus/icons-vue'
import { addOrUpdatePractice, addOrUpdateArticle } from '@/api'
import type { AddOrUpdatePracticeDto } from '@/api/practice/types'
type ArticleType = ArticleTypeEnum.QUESTION | ArticleTypeEnum.PRACTICE
const { type } = defineProps<{
type: ArticleType
}>()
const textMap: Record<
ArticleType,
{ title: string; content: string; api: (data: any) => Promise<any> }
> = {
[ArticleTypeEnum.QUESTION]: {
title: '问题标题',
content: '请输入问题内容',
api: addOrUpdateArticle,
},
[ArticleTypeEnum.PRACTICE]: {
title: '实践标题',
content: '请输入实践内容',
api: addOrUpdatePractice,
},
}
const tagsStore = useTagsStore()
const { tagList } = storeToRefs(tagsStore)
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const selectTagsDialogRef =
useTemplateRef<InstanceType<typeof SelectTagsDialog>>('selectTagsDialogRef')
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
const [form, resetForm] = useResetData({
title: '',
content: '',
imgUrl: [],
releaseStatus: ReleaseStatusTypeEnum.PUBLISH,
mainTagId: '',
tagList: [],
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
})
const mainTagText = computed(() => {
return tagList.value.find((tag) => tag.id === Number(form.value.mainTagId))?.title
})
const subTagTextList = computed(() => {
return form.value.tagList.map((tag) => tagList.value.find((t) => t.id === tag)?.title)
})
const handleAddTag = () => {
selectTagsDialogRef.value?.open()
}
const handleFileChange = async (e: Event) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (file) {
const { data } = await uploadFile(file)
form.value.imgUrl.push(data.data[0].filePath)
}
}
const handleDeleteImg = (img: string) => {
form.value.imgUrl = form.value.imgUrl.filter((item) => item !== img)
}
const validateForm = () => {
if (!form.value.title) {
ElMessage.error('请输入实践标题')
return false
}
if (!form.value.content) {
ElMessage.error('请输入实践内容')
return false
}
if (!form.value.mainTagId) {
ElMessage.error('请选择主标签')
return false
}
return true
}
const transformForm = (releaseStatus: ReleaseStatusTypeEnum): AddOrUpdatePracticeDto => {
return {
...form.value,
releaseStatus,
faceUrl: form.value.imgUrl[0] || '',
imgUrl: form.value.imgUrl.join(','),
tagList: [form.value.mainTagId, ...form.value.tagList].map((item, index) => ({
sort: index,
tagId: Number(item),
})),
type: type,
}
}
const handlePublish = async (releaseStatus: ReleaseStatusTypeEnum) => {
if (!validateForm()) return
await textMap[type].api(transformForm(releaseStatus))
ElMessage.success(releaseStatus === ReleaseStatusTypeEnum.PUBLISH ? '发布成功' : '存草稿成功')
resetForm()
}
</script>
<style scoped></style>
...@@ -31,6 +31,8 @@ export interface PageSearchConfig<T extends PageSearchParams = PageSearchParams> ...@@ -31,6 +31,8 @@ export interface PageSearchConfig<T extends PageSearchParams = PageSearchParams>
pageField?: keyof T pageField?: keyof T
/** 页大小字段名 */ /** 页大小字段名 */
pageSizeField?: keyof T pageSizeField?: keyof T
/** 格式化列表数据 */
formatList?: (list: any[]) => any[]
} }
/** /**
...@@ -54,6 +56,7 @@ export function usePageSearch< ...@@ -54,6 +56,7 @@ export function usePageSearch<
defaultParams = {} as Omit<TParams, 'current' | 'size'>, defaultParams = {} as Omit<TParams, 'current' | 'size'>,
pageField = 'current' as keyof TParams, pageField = 'current' as keyof TParams,
pageSizeField = 'size' as keyof TParams, pageSizeField = 'size' as keyof TParams,
formatList = (list: any[]) => list,
} = config } = config
const loading = shallowRef(false) const loading = shallowRef(false)
...@@ -74,7 +77,7 @@ export function usePageSearch< ...@@ -74,7 +77,7 @@ export function usePageSearch<
const searchData = params ? { ...searchParams.value, ...params } : searchParams.value const searchData = params ? { ...searchParams.value, ...params } : searchParams.value
const { data } = await searchApi(searchData as TParams) const { data } = await searchApi(searchData as TParams)
list.value = data.list || [] list.value = formatList(data.list || [])
total.value = data.total || 0 total.value = data.total || 0
} catch (error) { } catch (error) {
console.log('分页搜索失败:', error) console.log('分页搜索失败:', error)
......
...@@ -81,7 +81,7 @@ export default defineComponent( ...@@ -81,7 +81,7 @@ export default defineComponent(
</el-form-item> </el-form-item>
<el-form-item label="图片" prop="faceUrl"> <el-form-item label="图片" prop="faceUrl">
{/* @ts-ignore */} {/* @ts-ignore */}
<UploadFile v-model={form.value.faceUrl} /> <UploadFile v-model={form.value.imgUrl} />
</el-form-item> </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">
......
<template>
<div class="min-h-screen px-20">
<!-- 主内容区 -->
<ActionButtons
v-model="articleDetail"
@scrollToCommentBox="commentRef?.scrollToCommentBox()"
></ActionButtons>
<div class="lg:col-span-3">
<!-- 帖子主体 -->
<ArticleContent :articleDetail="articleDetail" />
<!-- 评论区 -->
<Comment ref="commentRef" :id="id" v-model:total="articleDetail.replyCount" />
</div>
</div>
</template>
<script lang="ts" setup>
import { getArticleDetail, type ArticleItemDto } from '@/api'
import ActionButtons from './components/actionButtons.vue'
import Comment from '@/components/common/Comment/index.vue'
import ArticleContent from '@/components/common/ArticleContent/index.vue'
const commentRef = useTemplateRef<typeof Comment | null>('commentRef')
const route = useRoute()
const id = route.params.articleId as string
const articleDetail = ref({} as ArticleItemDto)
const initPage = () => {
Promise.allSettled([getArticleDetail(id)]).then(([r1]) => {
if (r1.status === 'fulfilled') {
articleDetail.value = r1.value.data
}
})
}
onMounted(async () => {
initPage()
})
</script>
<template>
<div v-loading="loading" class="px-20">
<div class="lg:col-span-3 mb-20">
<ArticleContent :articleDetail="articleDetail" />
</div>
<!-- 底部fixed -->
<div class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-lg">
<div class="max-w-7xl mx-auto px-6 py-4 flex justify-center items-center gap-4">
<el-button type="primary" @click="handleAgree" class="min-w-[120px]"> 通过 </el-button>
<el-button type="danger" @click="handleReject" class="min-w-[120px]"> 驳回 </el-button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import ArticleContent from '@/components/common/ArticleContent/index.vue'
import { type ArticleItemDto } from '@/api'
import { getArticleDetail, auditArticle } from '@/api'
import { AuditStatusEnum } from '@/constants/enums'
const route = useRoute()
const router = useRouter()
const id = route.params.id as string
const articleDetail = ref({} as ArticleItemDto)
const loading = ref(false)
const handleAgree = async () => {
await auditArticle({
articleId: Number(id),
auditResult: AuditStatusEnum.AGREED,
})
ElMessage.success('审核通过')
router.push('/')
}
const handleReject = async () => {
const remark = await ElMessageBox.prompt('请输入审核驳回原因', '审核驳回', {
confirmButtonText: '确定',
cancelButtonText: '取消',
})
if (!remark.value) {
ElMessage.warning('请输入审核驳回原因')
return
}
await auditArticle({
articleId: Number(id),
auditResult: AuditStatusEnum.REJECTED,
auditRemark: remark.value,
})
ElMessage.success('审核驳回')
}
onMounted(async () => {
loading.value = true
const { data } = await getArticleDetail(id)
articleDetail.value = data
loading.value = false
})
</script>
...@@ -56,10 +56,11 @@ const menuList: MenuItem[] = [ ...@@ -56,10 +56,11 @@ const menuList: MenuItem[] = [
{ path: '/backend/manager', title: '企业文化管理员' }, { path: '/backend/manager', title: '企业文化管理员' },
{ path: '/backend/tags', title: '官方标签' }, { path: '/backend/tags', title: '官方标签' },
{ path: '/backend/carousel', title: '轮播图设置' }, { path: '/backend/carousel', title: '轮播图设置' },
{ path: '/backend/topic-admin', title: '专栏——管理员' }, // { path: '/backend/topic-admin', title: '专栏——管理员' },
{ path: '/backend/columnSettings', title: '专栏——栏目管理' }, { path: '/backend/columnSettings', title: '专栏——栏目管理' },
{ path: '/backend/interview-admin', title: '专访——管理员' }, // { path: '/backend/interview-admin', title: '专访——管理员' },
{ path: '/backend/interviewSettings', title: '专访——栏目管理' }, { path: '/backend/interviewSettings', title: '专访——栏目管理' },
{ path: '/backend/videoSettings', title: '视频——栏目管理' },
{ path: '/backend/videoManage', title: '视频管理' }, { path: '/backend/videoManage', title: '视频管理' },
{ path: '/backend/caseManage', title: 'YAYA案例库管理' }, { path: '/backend/caseManage', title: 'YAYA案例库管理' },
{ path: '/backend/goodsManage', title: '积分商城——商品配置' }, { path: '/backend/goodsManage', title: '积分商城——商品配置' },
......
<template>
<div class="official-tag-page">
<!-- 搜索栏 -->
<div class="search-section">
<div class="flex-1 flex gap-2">
<el-input
v-model="searchParams.title"
placeholder="请输入栏目标题"
class="w-200px"
></el-input>
<el-select
v-model="searchParams.status"
placeholder="请选择发布状态"
class="search-select"
clearable
>
<el-option label="发布" :value="1" />
<el-option label="隐藏" :value="0" />
</el-select>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div>
<div class="flex justify-end">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
</el-button>
<el-button type="primary" @click="handleBatchPublish"> 批量发布/隐藏 </el-button>
<el-button type="danger" @click="handleBatchDelete"> 批量删除 </el-button>
</div>
</div>
<!-- 表格区域 -->
<div class="table-section">
<!-- 表格 -->
<div class="table-wrapper">
<el-table
v-loading="loading"
:data="list"
height="100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="sort" label="栏目顺序" width="180"> </el-table-column>
<el-table-column prop="title" label="栏目名称" min-width="200" />
<el-table-column prop="postCount" label="栏目帖子数量" min-width="200" />
<el-table-column prop="color" label="颜色" width="300">
<template #default="{ row }">
<div class="color-cell">
<div class="color-block" :style="{ backgroundColor: row.color }" />
<span class="color-text">{{ row.color }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="createUserId" label="创建人" min-width="200" />
<el-table-column prop="createTime" label="创建时间" min-width="200">
<template #default="{ row }">
{{ dayjs(row.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="200">
<template #default="{ row }">
<el-switch
:model-value="row.status"
:active-value="1"
:inactive-value="0"
@change="handleStatusChange(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:total="total"
:page-sizes="[10, 20, 30]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="changePageSize"
@current-change="goToPage"
/>
</div>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-color-picker v-model="form.color" />
<span class="color-value">{{ form.color }}</span>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="100" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">
<el-icon class="btn-icon"><Upload /></el-icon>
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { Search, Plus, Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks'
import { listOfCultureColumn, addOrUpdateColumn, deleteColumn, hideColumn } from '@/api/backend'
import type { FormInstance, FormRules } from 'element-plus'
import type { BackendColumnListItemDto, AddOrUpdateColumnDto } from '@/api/backend'
import dayjs from 'dayjs'
const { loading, list, total, reset, goToPage, changePageSize, refresh, searchParams, search } =
usePageSearch(listOfCultureColumn, {
defaultParams: {
type: 'video',
},
})
// 对话框
const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑标签' : '新增标签'))
const formRef = ref<FormInstance>()
// 表单数据
const [form, resetForm] = useResetData<AddOrUpdateColumnDto>({
title: '',
color: '#000000',
id: undefined,
sort: 0,
type: 'video',
})
// 表单验证规则
const formRules: FormRules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
color: [{ required: true, message: '请选择颜色', trigger: 'change' }],
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
}
// 新增
const handleAdd = () => {
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = (row: BackendColumnListItemDto) => {
resetForm()
form.value = {
title: row.title,
color: row.color,
id: row.id,
sort: row.sort,
type: 'video',
}
dialogVisible.value = true
}
// 状态改变
const handleStatusChange = async (row: BackendColumnListItemDto) => {
await hideColumn([row.id])
refresh()
}
// 删除
const handleDelete = async (row: BackendColumnListItemDto) => {
try {
await ElMessageBox.confirm(`确定要删除标签"${row.title}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
await deleteColumn([row.id])
ElMessage.success('删除成功')
refresh()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (form.value.id) {
await addOrUpdateColumn(form.value)
} else {
await addOrUpdateColumn(form.value)
}
ElMessage.success(form.value.id ? '编辑成功' : '新增成功')
dialogVisible.value = false
if (form.value.id) {
search()
} else {
refresh()
}
} catch (error) {
console.error('表单验证失败:', error)
}
}
const selectedRows = ref<BackendColumnListItemDto[]>([])
// 选择
const handleSelectionChange = (selection: BackendColumnListItemDto[]) => {
selectedRows.value = selection
}
// 批量发布/隐藏
const handleBatchPublish = async () => {
await hideColumn(selectedRows.value.map((item) => item.id))
refresh()
selectedRows.value = []
ElMessage.success('发布/隐藏成功')
}
// 批量删除
const handleBatchDelete = async () => {
await deleteColumn(selectedRows.value.map((item) => item.id))
refresh()
selectedRows.value = []
ElMessage.success('删除成功')
}
</script>
<style scoped lang="scss">
.official-tag-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
// 搜索区域
.search-section {
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
gap: 12px;
flex-shrink: 0;
.search-select {
width: 200px;
}
}
// 表格区域
.table-section {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
min-height: 0;
}
.table-wrapper {
flex: 1;
min-height: 0;
.color-cell {
display: flex;
align-items: center;
gap: 12px;
.color-block {
width: 100%;
height: 36px;
border-radius: 4px;
border: 1px solid #e5e7eb;
flex: 1;
}
.color-text {
color: #fff;
font-size: 14px;
font-weight: 500;
position: absolute;
left: 50%;
transform: translateX(-50%);
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding-top: 16px;
flex-shrink: 0;
}
// 对话框内的颜色显示
.color-value {
margin-left: 12px;
color: #606266;
font-family: monospace;
}
.btn-icon {
margin-right: 4px;
}
</style>
...@@ -146,7 +146,7 @@ const handleClickItem = (item: ArticleItemDto) => { ...@@ -146,7 +146,7 @@ const handleClickItem = (item: ArticleItemDto) => {
if (item.type === ArticleTypeEnum.VIDEO) { if (item.type === ArticleTypeEnum.VIDEO) {
router.push(`/videoDetail/${item.id}`) router.push(`/videoDetail/${item.id}`)
} else { } else {
router.push(`/postDetail/${item.id}`) router.push(`/articleDetail/${item.id}`)
} }
} }
......
<template> <template>
<div class="main-container"> <div class="main-container">
<div class="banner mb-3"> <div class="banner mb-3 w-full aspect-[16/6] overflow-hidden">
<el-carousel height="410px" class="shadow-lg rounded-lg"> <el-carousel class="h-full w-full shadow-lg rounded-lg">
<el-carousel-item v-for="(item, index) in carouselList" :key="index"> <el-carousel-item v-for="(item, index) in carouselList" :key="index" class="h-full w-full">
<el-image :src="item.assetUrl" class="w-full h-full object-cover" /> <el-image :src="item.assetUrl" class="w-full h-full object-cover" />
</el-carousel-item> </el-carousel-item>
</el-carousel> </el-carousel>
...@@ -273,7 +273,7 @@ const levelContainerRef = useTemplateRef<HTMLElement>('levelContainerRef') ...@@ -273,7 +273,7 @@ const levelContainerRef = useTemplateRef<HTMLElement>('levelContainerRef')
const dailySignBtnRef = useTemplateRef<HTMLElement>('dailySignBtnRef') const dailySignBtnRef = useTemplateRef<HTMLElement>('dailySignBtnRef')
const { handleBackTop } = useScrollTop(levelContainerRef) const { handleBackTop } = useScrollTop(levelContainerRef)
const { triggerAnimation } = useHintAnimation(dailySignBtnRef, { const { triggerAnimation } = useHintAnimation(dailySignBtnRef, {
classes: ['scale-bounce', 'highlight', 'shake-x'], classes: ['scale-bounce', 'highlight', 'shake-y'],
}) })
const getThirdLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => { const getThirdLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => {
...@@ -361,9 +361,13 @@ const onDailySign = async () => { ...@@ -361,9 +361,13 @@ const onDailySign = async () => {
} }
const handleTask = (item: TaskItemDto) => { const handleTask = (item: TaskItemDto) => {
console.log(item)
if (item.currentCount === item.limitCount) return if (item.currentCount === item.limitCount) return
if ((item.taskKey = 'VALID_COMMENT')) { if (item.svgName === 'VALID_COMMENT1') {
router.push(`/homePage/homeTab`) router.push(`/homePage/homeTab`)
} else if (item.svgName === 'daily_sign') {
handleBackTop()
triggerAnimation()
} else { } else {
console.log(item) console.log(item)
// if (item.svgName === 'svgName') { // if (item.svgName === 'svgName') {
...@@ -411,6 +415,11 @@ provide(TABS_REF_KEY, tabsRef) ...@@ -411,6 +415,11 @@ provide(TABS_REF_KEY, tabsRef)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.main-container { .main-container {
.banner {
:deep(.el-carousel__container) {
height: 100% !important;
}
}
padding-bottom: 100px; padding-bottom: 100px;
.tabs-container { .tabs-container {
background: linear-gradient(to right, #d1f3ff 0%, #bebeff 100%); background: linear-gradient(to right, #d1f3ff 0%, #bebeff 100%);
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
v-for="i in item.yaColumnVoList" v-for="i in item.yaColumnVoList"
:key="i.articleId" :key="i.articleId"
class="group cursor-pointer" class="group cursor-pointer"
@click="router.push(`/postDetail/${i.articleId}`)" @click="router.push(`/articleDetail/${i.articleId}`)"
> >
<div class="relative mb-3 overflow-hidden rounded-lg"> <div class="relative mb-3 overflow-hidden rounded-lg">
<img <img
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
v-for="i in item.yaColumnVoList" v-for="i in item.yaColumnVoList"
:key="i.articleId" :key="i.articleId"
class="group cursor-pointer" class="group cursor-pointer"
@click="router.push(`/postDetail/${i.articleId}`)" @click="router.push(`/articleDetail/${i.articleId}`)"
> >
<div class="relative mb-3 overflow-hidden rounded-lg"> <div class="relative mb-3 overflow-hidden rounded-lg">
<img <img
......
<template> <template>
<div> <div>
<!-- 发布区域 --> <!-- 发布区域 -->
<PublishPractice /> <PublishPractice :type="ArticleTypeEnum.PRACTICE" />
<!-- 标签导航 --> <!-- 标签导航 -->
<div class="bg-white p-4 mb-6 rounded-lg shadow-sm"> <div class="bg-white p-4 mb-6 rounded-lg shadow-sm">
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
<div class="divide-y bg-#fff"> <div class="divide-y bg-#fff">
<div <div
@click="router.push(`/postDetail/${item.id}`)" @click="router.push(`/articleDetail/${item.id}`)"
v-for="item in list" v-for="item in list"
:key="item.id" :key="item.id"
class="p-4 hover:bg-gray-50 transition-colors cursor-pointer" class="p-4 hover:bg-gray-50 transition-colors cursor-pointer"
...@@ -149,7 +149,8 @@ import { usePageSearch, useScrollTop } from '@/hooks' ...@@ -149,7 +149,8 @@ import { usePageSearch, useScrollTop } from '@/hooks'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { TABS_REF_KEY } from '@/constants' import { TABS_REF_KEY } from '@/constants'
import { useTagsStore } from '@/stores/tags' import { useTagsStore } from '@/stores/tags'
import PublishPractice from './publishPractice.vue' import PublishPractice from '@/components/common/PublishBox/index.vue'
import { ArticleTypeEnum } from '@/constants'
const tagsStore = useTagsStore() const tagsStore = useTagsStore()
const { tagList } = storeToRefs(tagsStore) const { tagList } = storeToRefs(tagsStore)
......
<template>
<div>
<div v-loading="loading" v-if="list.length">
<div class="w-full max-w-6xl mx-auto">
<div
v-for="(item, index) in list"
:key="index"
class="bg-white rounded-lg shadow-sm mb-6 overflow-hidden"
:style="{ '--dynamic-color': item.color }"
>
<div
class="flex items-center justify-between pr-4 pl-4 pt-2 pb-2 bg-green-50 border-b border-green-100"
:style="{ backgroundColor: item.color, '--dynamic-color': item.color }"
>
<h3 class="text-lg font-medium text-gray-800 flex items-center">
<span class="w-1 h-5 mr-2 bg-#444"></span>
{{ item.title }}
</h3>
<div class="flex items-center cursor-pointer">
<span class="mr-1 text-14px color-#606266">查看更多 >></span>
</div>
</div>
<div class="p-4">
<div v-if="item.yaColumnVoList.length" class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div
v-for="i in item.yaColumnVoList"
:key="i.articleId"
class="group cursor-pointer"
@click="router.push(`/articleDetail/${i.articleId}`)"
>
<div class="relative mb-3 overflow-hidden rounded-lg">
<img
:src="i.faceUrl"
class="w-full aspect-[5/3] object-cover group-hover:scale-105 transition-transform duration-300"
/>
<div
v-if="i.isRecommend"
class="absolute top--1 left--1 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 font-medium text-gray-800 mb-2 transition-colors">
{{ i.title }}
</h3>
<p class="text-sm text-gray-500 mb-3 line-clamp-2">
{{ i.content }}
</p>
<div class="flex items-center justify-between text-xs text-gray-500">
<div class="flex items-center space-x-4">
<span class="flex items-center">
<el-icon class="mr-1"><View /></el-icon>
{{ i.viewCount }}
</span>
<span class="flex items-center">
<el-icon class="mr-1"><ChatDotRound /></el-icon>
{{ i.replyCount }}
</span>
<span class="flex items-center">
<el-icon class="mr-1"><Star /></el-icon>
{{ i.collectCount }}
</span>
</div>
<span>{{ dayjs(i.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
</div>
</div>
</div>
<div v-else class="flex items-center justify-center h-48">
<el-empty description="暂无数据" />
</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-lg shadow-sm border border-gray-100 p-3"
>
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:page-sizes="[15, 30, 45, 60]"
layout="prev, pager, next, jumper, total"
:total="total"
class="custom-pagination"
@current-change="
(e) => {
;(handleBackTop(), goToPage(e))
}
"
@size-change="changePageSize"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<template v-else>
<div class="flex items-center justify-center h-full">
<el-empty description="暂无数据" />
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { View, ChatDotRound, Star } from '@element-plus/icons-vue'
import { getVideoList } from '@/api'
import { usePageSearch, useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants'
import dayjs from 'dayjs'
import { useRouter } from 'vue-router'
const router = useRouter()
const tabsRef = inject(TABS_REF_KEY)
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const { list, total, searchParams, goToPage, changePageSize, loading, refresh } = usePageSearch(
getVideoList,
{
defaultSize: 3,
immediate: false,
},
)
defineExpose({
refresh: () => {
searchParams.value.current = 0
refresh()
},
})
</script>
<style scoped></style>
...@@ -28,12 +28,13 @@ import { Refresh } from '@element-plus/icons-vue' ...@@ -28,12 +28,13 @@ import { Refresh } from '@element-plus/icons-vue'
import ColumnList from './components/columnList.vue' import ColumnList from './components/columnList.vue'
import InterviewList from './components/interviewList.vue' import InterviewList from './components/interviewList.vue'
import PracticeList from './components/practiceList.vue' import PracticeList from './components/practiceList.vue'
import VideoList from './components/videoList.vue'
const tabs = [ const tabs = [
{ label: '专栏', value: '专栏', component: ColumnList }, { label: '专栏', value: '专栏', component: ColumnList },
{ label: '实践', value: '实践', component: PracticeList }, { label: '实践', value: '实践', component: PracticeList },
{ label: '专访', value: '专访', component: InterviewList }, { label: '专访', value: '专访', component: InterviewList },
{ label: '视频', value: '视频', component: () => h('h1', '11') }, { label: '视频', value: '视频', component: VideoList },
] ]
const activeTab = ref('专栏') const activeTab = ref('专栏')
......
...@@ -226,7 +226,7 @@ const handleClick = (item: ArticleListItemDto) => { ...@@ -226,7 +226,7 @@ const handleClick = (item: ArticleListItemDto) => {
if (item.type === ArticleTypeEnum.VIDEO) { if (item.type === ArticleTypeEnum.VIDEO) {
router.push(`/videoDetail/${item.id}`) router.push(`/videoDetail/${item.id}`)
} else { } else {
router.push(`/postDetail/${item.id}`) router.push(`/articleDetail/${item.id}`)
} }
} }
......
...@@ -95,7 +95,7 @@ const handleView = (item: SelfCollectDetailDto) => { ...@@ -95,7 +95,7 @@ const handleView = (item: SelfCollectDetailDto) => {
if (item.type === ArticleTypeEnum.VIDEO) { if (item.type === ArticleTypeEnum.VIDEO) {
router.push(`/videoDetail/${item.id}`) router.push(`/videoDetail/${item.id}`)
} else { } else {
router.push(`/postDetail/${item.id}`) router.push(`/articleDetail/${item.id}`)
} }
} }
</script> </script>
...@@ -93,7 +93,7 @@ const handleView = (item: SelfPraiseDetailDto) => { ...@@ -93,7 +93,7 @@ const handleView = (item: SelfPraiseDetailDto) => {
if (item.type === ArticleTypeEnum.VIDEO) { if (item.type === ArticleTypeEnum.VIDEO) {
router.push(`/videoDetail/${item.id}`) router.push(`/videoDetail/${item.id}`)
} else { } else {
router.push(`/postDetail/${item.id}`) router.push(`/articleDetail/${item.id}`)
} }
} }
</script> </script>
...@@ -93,7 +93,7 @@ const handleView = (item: SelfPublishDetailDto) => { ...@@ -93,7 +93,7 @@ const handleView = (item: SelfPublishDetailDto) => {
if (item.type === ArticleTypeEnum.VIDEO) { if (item.type === ArticleTypeEnum.VIDEO) {
router.push(`/videoDetail/${item.id}`) router.push(`/videoDetail/${item.id}`)
} else { } else {
router.push(`/postDetail/${item.id}`) router.push(`/articleDetail/${item.id}`)
} }
} }
const handleDelete = async (articleId: number) => { const handleDelete = async (articleId: number) => {
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<!-- 顶部操作按钮 --> <!-- 顶部操作按钮 -->
<div class="absolute top-4 right-4 flex gap-2"> <div class="absolute top-4 right-4 flex gap-2">
<el-button type="info" plain size="small">清除缓存</el-button> <el-button type="info" plain size="small">清除缓存</el-button>
<el-button type="info" plain size="small">切换账号</el-button> <el-button type="info" plain size="small" @click="handleSwitchAccount">切换账号</el-button>
<el-button type="info" plain size="small" @click="handleAdmin">后台管理</el-button> <el-button type="info" plain size="small" @click="handleAdmin">后台管理</el-button>
</div> </div>
</div> </div>
...@@ -111,7 +111,7 @@ import SelfCase from './components/selfCase.vue' ...@@ -111,7 +111,7 @@ import SelfCase from './components/selfCase.vue'
import SelfTask from './components/selfTask.vue' import SelfTask from './components/selfTask.vue'
import SelfComment from './components/selfComment.vue' import SelfComment from './components/selfComment.vue'
import SelfAudit from './components/selfAudit.vue' import SelfAudit from './components/selfAudit.vue'
import { hasOfficialAccount } from '@/api' import { generateLoginKey, hasOfficialAccount } from '@/api'
const editUserInfoRef = useTemplateRef<InstanceType<typeof EditUserInfo>>('editUserInfoRef') const editUserInfoRef = useTemplateRef<InstanceType<typeof EditUserInfo>>('editUserInfoRef')
const userStore = useUserStore() const userStore = useUserStore()
...@@ -167,6 +167,17 @@ const getIsOfficial = () => { ...@@ -167,6 +167,17 @@ const getIsOfficial = () => {
}, 1000) }, 1000)
} }
const handleSwitchAccount = async () => {
console.log('切换账号')
const { data } = await generateLoginKey({
cutEmail: 'SzTrain@yswg.com.cn',
timestamp: Date.now(),
type: 2,
userId: userInfo.value.id,
})
console.log(data)
}
const handleAdmin = () => { const handleAdmin = () => {
window.open('/backend') window.open('/backend')
} }
......
<template>
<el-dialog
v-model="dialogVisible"
width="420px"
:show-close="true"
:close-on-click-modal="false"
:lock-scroll="true"
>
<!-- 标题 -->
<template #header>
<div class="text-center text-18px font-medium text-gray-800">
给作者投上
<span class="text-sky-500 font-bold">{{ selectedAmount }}</span>
枚YA币
</div>
</template>
<!-- 内容 -->
<div class="py-4">
<!-- 金额选择 -->
<div class="flex justify-center gap-6 mb-6">
<div
v-for="option in rewardOptions"
:key="option.amount"
class="reward-option"
:class="{ 'reward-option-active': option.selected }"
@click="selectAmount(option.amount)"
>
<div class="reward-character">
<img
v-if="option.amount === 1"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='45' fill='%23e0e0e0'/%3E%3Ctext x='50' y='65' font-size='40' text-anchor='middle' fill='%23666'%3E🪙%3C/text%3E%3C/svg%3E"
alt="1硬币"
class="w-full h-full"
/>
<img
v-else
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='35' cy='50' r='30' fill='%23ffd700'/%3E%3Ccircle cx='65' cy='50' r='30' fill='%23ffd700'/%3E%3Ctext x='50' y='65' font-size='30' text-anchor='middle' fill='%23fff'%3E🪙%3C/text%3E%3C/svg%3E"
alt="2硬币"
class="w-full h-full"
/>
</div>
<div class="reward-amount">{{ option.amount }}硬币</div>
</div>
</div>
<!-- 确认按钮 -->
<div class="flex justify-center">
<el-button type="primary" size="large" round class="w-120px" @click="handleConfirm">
确定
</el-button>
</div>
<!-- 余额提示 -->
<div class="text-center text-12px text-gray-400 mt-4">当前余额: {{ balance }}</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { addOrCancelArticleReward, getYaBiData } from '@/api'
interface RewardOption {
amount: number
icon: string
selected: boolean
}
const dialogVisible = ref(false)
const selectedAmount = ref(2)
const balance = ref(0)
const rewardOptions = ref<RewardOption[]>([
{ amount: 1, icon: '🪙', selected: false },
{ amount: 2, icon: '🪙🪙', selected: true },
])
// 暴露 open 方法
const open = () => {
dialogVisible.value = true
getYaBiDataFn()
}
const getYaBiDataFn = async () => {
const { data } = await getYaBiData()
balance.value = data.currentValue
}
// 选择金额
const selectAmount = (amount: number) => {
selectedAmount.value = amount
rewardOptions.value.forEach((option) => {
option.selected = option.amount === amount
})
}
// 确认打赏
const handleConfirm = async () => {
if (balance.value < selectedAmount.value) {
ElMessage.warning('余额不足,请先充值')
return
}
await addOrCancelArticleReward({
articleId: 1,
ayabi: selectedAmount.value,
})
ElMessage.success('打赏成功!')
dialogVisible.value = false
}
defineExpose({
open,
})
</script>
<style scoped>
/* 弹窗样式 */
/* 金额选项卡片 */
.reward-option {
width: 160px;
padding: 20px;
border: 2px dashed #e5e7eb;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.reward-option:hover {
border-color: #60a5fa;
background: #eff6ff;
}
.reward-option-active {
border: 2px solid #3b82f6;
background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
/* 角色图片 */
.reward-character {
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
}
.reward-character img {
width: 100%;
height: 100%;
object-fit: contain;
}
/* 金额文字 */
.reward-amount {
font-size: 14px;
font-weight: 500;
color: #6b7280;
}
.reward-option-active .reward-amount {
color: #3b82f6;
font-weight: 600;
}
/* 按钮样式 */
:deep(.el-button--primary) {
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
border: none;
font-weight: 500;
height: 44px;
font-size: 16px;
}
:deep(.el-button--primary:hover) {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
}
</style>
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