feat: 稿件评论

This commit is contained in:
rd
2025-08-08 15:55:11 +08:00
parent 62c029bb8a
commit 819fb8db8f
8 changed files with 431 additions and 180 deletions

View File

@ -1,175 +0,0 @@
<script lang="jsx">
import { Image, Spin, Button } from '@arco-design/web-vue';
import TextOverTips from '@/components/text-over-tips';
import { RESULT_LIST, ENUM_OPINION } from './constants';
import icon1 from '@/assets/img/creative-generation-workshop/icon-line.png';
const _iconMap = new Map([
[1, { icon: <icon-check-circle-fill size={16} class="color-#25C883" /> }],
[2, { icon: <icon-exclamation-circle-fill size={16} class="color-#F64B31" /> }],
[3, { icon: <icon-exclamation-circle-fill size={16} class="color-#FFAE00" /> }],
[4, { icon: <icon-exclamation-circle-fill size={16} class="color-#939499" /> }],
]);
const data1 = [
{
label: '色情检测',
level: 1,
},
{
label: '涉政检测',
level: 2,
},
{
label: '涉政正负向检测',
level: 3,
},
{
label: '涉政正负向检测',
level: 3,
},
{
label: '涉政正负向检测',
level: 3,
},
{
label: '涉政正负向检测',
level: 3,
},
{
label: '暴恐检测',
level: 4,
},
{
label: '暴恐检测',
level: 4,
},
];
export default {
props: {
isExpand: {
type: Boolean,
default: true,
},
},
emits: ['toggle'],
setup(props, { emit, expose }) {
return () => (
<section class="py-16px absolute right-16px w-440px h-full">
<div class="ai-suggest-box p-24px">
<div class="mb-16px w-full flex justify-between">
<div class="mb-24px relative w-fit">
<span class="ai-text">AI 智能审核</span>
<img src={icon1} class="w-80px h-10.8px absolute bottom-1px left--9px" />
</div>
<icon-menu-unfold
size={20}
class="color-#55585F cursor-pointer hover:color-#6D4CFE"
onClick={() => emit('toggle', false)}
/>
</div>
<div class="result-box p-16px rounded-8px mb-16px">
<div class="flex items-center justify-between mb-16px">
<p class="cts bold !color-#000 !text-16px">审核结果</p>
<Button type="text" class="!color-#6D4CFE hover:!color-#8A70FE">
展开详情
</Button>
</div>
<div class="flex items-center">
{RESULT_LIST.map((item, index) => (
<div class="flex flex-col justify-center items-center flex-1 result-item" key={index}>
<span class={`s1 ${item.class}`}>30</span>
<span class="cts mt-4px !lh-20px !text-12px !color-#737478">{item.label}</span>
</div>
))}
</div>
</div>
<div class="result-box p-16px rounded-8px mb-16px">
<p class="cts bold !color-#000 !text-16px mb-16px">敏感词检测</p>
<div class="grid grid-cols-3 gap-x-24px gap-y-8px">
{
data1.map((item, index) => (
<div class="audit-item" key={index}>
<div class="flex items-center">
{_iconMap.get(item.level)?.icon}
<TextOverTips context={item.label} class="cts ml-4px" />
</div>
</div>
))
}
</div>
</div>
<div class="result-box p-16px rounded-8px mb-16px">
<p class="cts bold !color-#000 !text-16px mb-16px">图片内容违规检测</p>
<p class="cts bold !color-#000 !text-16px mb-16px">图片文字违规检测</p>
</div>
</div>
</section>
);
},
};
</script>
<style lang="scss" scoped>
.ai-suggest-box {
.cts {
color: #939499;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
&.bold {
font-family: $font-family-medium;
}
}
width: 440px;
height: fit-content;
max-height: 100%;
border-radius: 16px;
background: linear-gradient(126deg, #eef2fd 8.36%, #f5ebfe 49.44%, #fdebf3 90.52%);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
.ai-text {
font-family: $font-family-medium;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
background: linear-gradient(85deg, #7d419d 4.56%, #31353d 94.75%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.result-box {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(4px);
.result-item {
.s1 {
color: var(--Brand-6, #6d4cfe);
font-family: $font-family-manrope-regular;
font-size: 24px;
font-style: normal;
font-weight: 700;
line-height: 32px; /* 133.333% */
}
&:first-child {
position: relative;
&::after {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
width: 1px;
height: 32px;
background: var(--Border-1, #d7d7d9);
}
}
}
}
}
</style>

View File

@ -0,0 +1,266 @@
<script lang="jsx">
import { Image, Spin, Button, Input } from '@arco-design/web-vue';
import TextOverTips from '@/components/text-over-tips';
import SvgIcon from '@/components/svg-icon/index.vue';
import { RESULT_LIST, ENUM_OPINION, formatRelativeTime } from '../../constants';
import { postShareWorksComments, deleteShareWorksComments } from '@/api/all/generationWorkshop.ts';
import { exactFormatTime } from '@/utils/tools.ts';
import icon1 from '@/assets/img/creative-generation-workshop/icon-line.png';
import icon2 from '@/assets/img/creative-generation-workshop/icon-avatar-default.png';
import icon3 from '@/assets/img/error-img.png';
const _iconMap = new Map([
[1, { icon: <icon-check-circle-fill size={16} class="color-#25C883 flex-shrink-0" /> }],
[2, { icon: <icon-exclamation-circle-fill size={16} class="color-#F64B31 flex-shrink-0" /> }],
[3, { icon: <icon-exclamation-circle-fill size={16} class="color-#FFAE00 flex-shrink-0" /> }],
[4, { icon: <icon-exclamation-circle-fill size={16} class="color-#939499 flex-shrink-0" /> }],
]);
const data1 = [
{
label: '色情检测',
level: 1,
},
{
label: '涉政检测',
level: 2,
},
{
label: '涉政正负向检测',
level: 3,
},
{
label: '涉政正负向检测',
level: 3,
},
{
label: '涉政正负向检测',
level: 3,
},
{
label: '涉政正负向检测',
level: 3,
},
{
label: '暴恐检测',
level: 4,
},
{
label: '暴恐检测',
level: 4,
},
];
export default {
props: {
isExpand: {
type: Boolean,
default: true,
},
dataSource: {
type: Object,
default: () => {},
},
},
emits: ['toggle', 'updateComment', 'deleteComment'],
setup(props, { emit, expose }) {
const route = useRoute();
const isCollapse = ref(false);
const comment = ref('');
const isReplay = ref(false);
const commentId = ref(undefined);
const onComment = async () => {
const { code, data } = await postShareWorksComments(props.dataSource.id, route.params.shareCode, {
content: comment.value,
comment_id: commentId.value,
});
if (code === 200) {
emit('updateComment');
comment.value = '';
}
};
const onClearComment = () => {
isReplay.value = false;
commentId.value = undefined;
comment.value = '';
};
const deleteComment = async (comment_id) => {
emit('deleteComment', comment_id);
deleteShareWorksComments(props.dataSource.id, comment_id, route.params.shareCode);
};
const getCommentName = (item) => {
if (item.commenter_id === 0) {
return '佚名';
}
// 姓名脱敏保留首尾字符中间拼接6个****
const maskName = (name) => {
if (!name || name.length <= 1) return name; // 单字符不脱敏
return name[0] + '****' + name[name.length - 1];
};
// 手机号脱敏保留前3位和后4位中间4位替换为****
const maskMobile = (mobile) => mobile?.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2');
const maskedName = maskName(item.commenter?.name);
const maskedMobile = maskMobile(item.commenter?.mobile);
return maskedName || maskedMobile;
};
return () => (
<section class="py-16px absolute right-16px w-440px h-full overflow-hidden">
<div class="ai-suggest-box py-24px flex flex-col">
<div class="mb-16px w-full flex justify-between px-24px">
<div class="relative w-fit">
<span class="ai-text">AI 智能审核</span>
<img src={icon1} class="w-80px h-10.8px absolute bottom-1px left--9px" />
</div>
<icon-menu-unfold
size={20}
class="color-#55585F cursor-pointer hover:color-#6D4CFE"
onClick={() => emit('toggle', false)}
/>
</div>
{/**主体 */}
<div class="flex-1 overflow-y-auto px-24px">
<div class="result-box p-16px rounded-8px">
<div class="flex items-center justify-between mb-16px">
<p class="cts bold !color-#000 !text-16px">审核结果</p>
<Button
type="text"
class="!color-#6D4CFE hover:!color-#8A70FE"
onClick={() => (isCollapse.value = !isCollapse.value)}
>
{isCollapse.value ? '展开详情' : '收起详情'}
</Button>
</div>
<div class="flex items-center">
{RESULT_LIST.map((item, index) => (
<div class="flex flex-col justify-center items-center flex-1 result-item" key={index}>
<span class={`s1 ${item.class}`}>30</span>
<span class="cts mt-4px !lh-20px !text-12px !color-#737478">{item.label}</span>
</div>
))}
</div>
</div>
{/**敏感词检测 */}
<div class={`collapse-box overflow-hidden ${isCollapse.value ? 'h-0 ' : 'h-auto'}`}>
<div class="result-box p-16px rounded-8px mt-16px">
<p class="cts bold !color-#000 !text-16px mb-16px">敏感词检测</p>
<div class="grid grid-cols-3 gap-x-24px gap-y-8px">
{data1.map((item, index) => (
<div class="audit-item" key={index}>
<div class="flex items-center h-20px">
{_iconMap.get(item.level)?.icon}
<TextOverTips context={item.label} class="cts ml-4px !color-#000" />
</div>
</div>
))}
</div>
</div>
<div class="result-box p-16px rounded-8px mt-16px">
<p class="cts bold !color-#000 !text-16px mb-16px">图片内容违规检测</p>
<div class="grid grid-cols-3 gap-x-24px gap-y-8px mb-16px">
{data1.map((item, index) => (
<div class="audit-item" key={index}>
<div class="flex items-center h-20px">
{_iconMap.get(item.level)?.icon}
<TextOverTips context={item.label} class="cts ml-4px !color-#000" />
</div>
</div>
))}
</div>
<p class="cts bold !color-#000 !text-16px mb-16px">图片文字违规检测</p>
<div class="grid grid-cols-3 gap-x-24px gap-y-8px">
{data1.map((item, index) => (
<div class="audit-item" key={index}>
<div class="flex items-center h-20px">
{_iconMap.get(item.level)?.icon}
<TextOverTips context={item.label} class="cts ml-4px !color-#000" />
</div>
</div>
))}
</div>
</div>
</div>
{/* 评论与回复 */}
<div class="comment-box">
<p class="my-16px">
<span class="cts bold cm !text-16px !lh-24px mr-8px">评论</span>
{props.dataSource.comments?.length > 0 && (
<span class="cts !text-16px !lh-24px bold">{props.dataSource.comments?.length}</span>
)}
</p>
<div class="comment-list flex flex-col my-16px rounded-8px">
{props.dataSource.comments?.map((item) => (
<div class="comment-item flex px-12px py-8px" key={item.id}>
<Image
src={item.commenter_id === 0 ? icon2 : item.commenter?.head_image}
width={40}
height={40}
preview={false}
fit="cover"
class="rounded-50% mr-13px"
v-slots={{
error: () => <img src={icon3} class="w-40 h-40 rounded-50%" />,
}}
/>
<div class="flex flex-col flex-1">
<div class="flex justify-between">
<p class="mb-4px">
<span class="cts !color-#211F24 mr-8px">{getCommentName(item)}</span>
<span class="cts !color-#939499">{formatRelativeTime(item.created_at)}</span>
</p>
<div class="flex items-center">
<SvgIcon
name="svg-comment"
size={16}
class="color-#55585F mr-12px cursor-pointer hover:color-#6D4CFE"
/>
<icon-delete
class="cursor-pointer color-#55585F hover:color-#6D4CFE"
size={16}
onClick={() => deleteComment(item.id)}
/>
</div>
</div>
<p class="cts !color-#211F24">{item.content}</p>
</div>
</div>
))}
</div>
<div>
<Input size="large" placeholder="输入评论" v-model={comment.value} onPressEnter={onComment} />
{comment.value && (
<div class="flex justify-end mt-12px">
<Button type="outline" class="mr-12px rounded-8px" size="medium" onClick={onClearComment}>
取消
</Button>
<Button type="primary" class="rounded-8px" size="medium" onClick={onComment}>
发送
</Button>
</div>
)}
</div>
</div>
</div>
</div>
</section>
);
},
};
</script>
<style lang="scss" scoped>
@import './style.scss';
</style>

View File

@ -0,0 +1,86 @@
.ai-suggest-box {
width: 440px;
height: fit-content;
max-height: 100%;
border-radius: 16px;
background: linear-gradient(126deg, #eef2fd 8.36%, #f5ebfe 49.44%, #fdebf3 90.52%);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
.cts {
color: #939499;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
&.bold {
font-family: $font-family-medium;
}
}
.ai-text {
font-family: $font-family-medium;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
background: linear-gradient(85deg, #7d419d 4.56%, #31353d 94.75%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.result-box {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(4px);
.result-item {
.s1 {
color: var(--Brand-6, #6d4cfe);
font-family: $font-family-manrope-regular;
font-size: 24px;
font-style: normal;
font-weight: 700;
line-height: 32px;
}
&:first-child {
position: relative;
&::after {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
width: 1px;
height: 32px;
background: var(--Border-1, #d7d7d9);
}
}
}
}
.collapse-box {
transition: all 0.3s;
}
.comment-box {
.cm {
background: linear-gradient(85deg, #7d419d 4.56%, #31353d 94.75%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
:deep(.arco-input-wrapper) {
padding: 8px 16px;
height: 38px;
border-radius: 8px !important;
background-color: rgba(255, 255, 255, 0.8) !important;
color: #211f24 !important;
&:hover {
border-color: #6d4cfe !important;
}
}
.comment-list {
backdrop-filter: blur(4px);
.comment-item {
&:not(:last-child) {
margin-bottom: 8px;
}
}
}
}
}

View File

@ -24,4 +24,44 @@ export const RESULT_LIST = [
export const ENUM_OPINION = {
wait: 0, // 待确认
confirm: 1, // 已确认
};
};
export const formatRelativeTime = (date: number): string => {
const target = dayjs(date * 1000);
if (!target.isValid()) return '';
const now = dayjs();
// 处理未来时间
if (target.isAfter(now)) return '刚刚';
const diffInMinutes = now.diff(target, 'minute');
const diffInHours = now.diff(target, 'hour');
const diffInDays = now.diff(target, 'day');
const diffInYears = now.diff(target, 'year');
// 1分钟内
if (diffInMinutes < 1) {
return '刚刚';
}
// 1分钟 ~ 1小时
else if (diffInMinutes < 60) {
return `${diffInMinutes}分钟前`;
}
// 1小时 ~ 24小时
else if (diffInHours < 24) {
return `${diffInHours}小时前`;
}
// 1天 ~ 30天
else if (diffInDays < 30) {
return `${diffInDays}天前`;
}
// 超过30天但不到1年
else if (diffInYears < 1) {
return target.format('MM-DD HH:mm');
}
// 超过1年
else {
return target.format('YYYY-MM-DD HH:mm');
}
};

View File

@ -1,11 +1,12 @@
<script lang="jsx">
import { Image, Spin, Button } from '@arco-design/web-vue';
import AiSuggest from './aiSuggest.vue';
import AiSuggest from './components/ai-suggest/';
import { getShareWorksList, getShareWorksDetail, patchShareWorksConfirm } from '@/api/all/generationWorkshop.ts';
import { convertVideoUrlToCoverUrl, exactFormatTime } from '@/utils/tools.ts';
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants.ts';
import { RESULT_LIST, ENUM_OPINION } from './constants';
import { useUserStore } from '@/stores';
import icon1 from '@/assets/logo.svg';
import icon2 from '@/assets/img/creative-generation-workshop/icon-confirm.png';
@ -15,6 +16,7 @@ export default {
setup(props, { emit, expose }) {
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const dataSource = ref({});
const shareWorks = ref({});
@ -54,6 +56,7 @@ export default {
try {
dataSource.value = {};
loading.value = true;
const { id } = route.params;
const { code, data } = await getShareWorksDetail(id, shareCode.value);
if (code === 200) {
@ -65,6 +68,14 @@ export default {
}
};
const updateCommentList = async () => {
const { id } = route.params;
const { code, data } = await getShareWorksDetail(id, shareCode.value);
if (code === 200) {
dataSource.value.comments = data.comments;
}
};
const getShareWorks = async () => {
const { code, data } = await getShareWorksList(shareCode.value);
if (code === 200) {
@ -138,7 +149,18 @@ export default {
dataSource.value.customer_opinion = ENUM_OPINION.confirm;
}
};
const onUpdateComment = () => {
updateCommentList();
};
const onDeleteComment = (id) => {
const index = dataSource.value.comments.findIndex((item) => item.id === id);
if (index === -1) return;
console.log({ index });
dataSource.value.comments.splice(index, 1);
};
const renderConfirmBtn = () => {
if (userStore.isLogin) return null;
if (dataSource.value.customer_opinion === ENUM_OPINION.confirm) return null;
return (
@ -228,7 +250,13 @@ export default {
)}
</section>
{isExpand.value && (
<AiSuggest isExpand={isExpand.value} onToggle={(expand) => (isExpand.value = expand)} />
<AiSuggest
isExpand={isExpand.value}
dataSource={dataSource.value}
onToggle={(expand) => (isExpand.value = expand)}
onUpdateComment={onUpdateComment}
onDeleteComment={onDeleteComment}
/>
)}
</div>
)}