feat: 客户分享链接列表

This commit is contained in:
rd
2025-08-07 18:05:27 +08:00
parent de55e00196
commit d07f773123
17 changed files with 678 additions and 115 deletions

View File

@ -0,0 +1,213 @@
<script lang="jsx">
import { Image, Spin, Button } from '@arco-design/web-vue';
import { getShareWorksDetail } from '@/api/all/generationWorkshop.ts';
import { convertVideoUrlToCoverUrl, exactFormatTime } from '@/utils/tools.ts';
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants.ts';
import icon1 from '@/assets/logo.svg';
import icon2 from '@/assets/img/creative-generation-workshop/icon-confirm.png';
import icon3 from '@/assets/img/creative-generation-workshop/icon-line.png';
const RESULT_LIST = [
{
label: '合规程度',
value: 'compliance_degree',
class: '!color-#6d4cfe',
},
{
label: '检验项',
value: 'compliance',
class: '!color-#211F24',
},
{
label: '高风险',
value: 'high_risk',
class: '!color-#F64B31',
},
{
label: '中风险',
value: 'medium_risk',
class: '!color-#FFAE00',
},
];
export default {
setup(props, { emit, expose }) {
const route = useRoute();
const dataSource = ref({});
const isExpand = ref(true);
const loading = ref(false);
const isPlaying = ref(false);
const videoRef = ref(null);
const videoUrl = ref('');
const coverImageUrl = ref('');
const isVideoLoaded = ref(false);
const images = ref([]);
const isVideo = computed(() => dataSource.value.type === EnumManuscriptType.Video);
const initData = () => {
const [fileOne, ...fileOthers] = dataSource.value.files ?? [];
if (isVideo.value) {
videoUrl.value = fileOne.url;
coverImageUrl.value = convertVideoUrlToCoverUrl(fileOne.url);
} else {
coverImageUrl.value = fileOne.url;
images.value = fileOthers;
}
};
const getDetail = async () => {
try {
loading.value = true;
const { id, shareCode } = route.params;
const { code, data } = await getShareWorksDetail(id, shareCode);
if (code === 200) {
dataSource.value = data;
initData();
}
} finally {
loading.value = false;
}
};
const renderMainImg = () => {
if (!coverImageUrl.value) return null;
if (isVideo.value) {
return (
<div class="main-video-box mb-16px relative overflow-hidden cursor-pointer" onClick={togglePlay}>
<video ref={videoRef} class="w-100% h-100% object-cover" onEnded={onVideoEnded}></video>
{!isPlaying.value && (
<>
<img src={coverImageUrl.value} class="w-100% h-100% object-cover absolute z-0 top-0 left-0" />
<div v-show={!isPlaying.value} class="play-icon"></div>
</>
)}
</div>
);
} else {
return (
<div class="main-img-box mb-16px relative overflow-hidden cursor-pointer">
<img src={coverImageUrl.value} class="w-100% h-100% object-cover absolute z-0 top-0 left-0" />
</div>
);
}
};
const togglePlay = () => {
if (!videoRef.value) return;
if (isPlaying.value) {
videoRef.value.pause();
} else {
if (!isVideoLoaded.value) {
videoRef.value.src = videoUrl.value;
isVideoLoaded.value = true;
}
videoRef.value.play();
}
isPlaying.value = !isPlaying.value;
};
const onVideoEnded = () => {
isPlaying.value = false;
};
onMounted(() => {
getDetail();
});
onBeforeUnmount(() => {
if (videoRef.value) {
videoRef.value.pause();
videoRef.value = null;
}
});
return () => {
return (
<div class="explore-page">
<header class="page-header">
<div class="content w-full px-24px flex items-center bg-#fff">
<img src={icon1} alt="" width={130} />
</div>
</header>
{loading.value ? (
<Spin spinning={loading.value} class="h-500px w-full flex justify-center items-center" size={60} />
) : (
<div class={`page-wrap relative ${isExpand.value ? 'expand' : ''}`}>
<div class="fold-box cursor-pointer" onClick={() => (isExpand.value = true)}>
<icon-menu-fold size={20} class="color-#55585F hover:color-#6D4CFE" />
</div>
<section class="explore-detail-wrap pt-32px pb-52px flex flex-col items-center">
<div class="flex justify-start flex-col w-full relative">
<p class="title mb-8px">{dataSource.value.title}</p>
<p class="cts color-#737478 mb-32px">
{exactFormatTime(dataSource.value.last_modified_at, 'YYYY年MM月DD日')}修改
</p>
{dataSource.value.customer_opinion === 1 && (
<img src={icon2} width={92} height={92} class="absolute right-0 bottom-0" />
)}
</div>
{renderMainImg()}
<p class="cts !color-#211F24">{dataSource.value.content}</p>
{/* 仅图片类型显示图片列表 */}
{!isVideo.value && (
<div class="desc-img-wrap mt-40px">
{images.value.map((item, index) => (
<div class="desc-img-box" key={index}>
<img src={item.url} class="w-100% h-100% object-cover" />
</div>
))}
</div>
)}
</section>
{isExpand.value && (
<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={icon3} 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={() => (isExpand.value = 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>
</section>
)}
</div>
)}
</div>
);
};
},
};
</script>
<style lang="scss" scoped>
@import './style.scss';
</style>

View File

@ -0,0 +1,151 @@
.explore-page {
position: relative;
padding-top: $navbar-height;
min-width: 1200px;
height: 100vh;
background: #fff;
display: flex;
flex-direction: column;
.page-header {
position: fixed;
left: 0;
right: 0;
top: 0;
z-index: 1000;
min-width: 1200px;
.content {
height: $navbar-height;
border-bottom: 1px solid var(--Border-1, #d7d7d9);
}
}
.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;
}
}
.page-wrap {
flex: 1;
display: flex;
justify-content: center;
.explore-detail-wrap {
min-height: 500px;
width: 684px;
.title {
color: var(--Text-1, #211f24);
font-family: $font-family-medium;
font-size: 28px;
font-style: normal;
font-weight: 400;
line-height: 40px; /* 142.857% */
}
}
.fold-box {
width: 40px;
height: 40px;
border-radius: 30px;
border: 1px solid var(--Border-1, #d7d7d9);
background: #fff;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.15);
position: absolute;
right: 16px;
top: 32px;
display: flex;
justify-content: center;
align-items: center;
}
.main-video-box {
width: 320px;
height: auto;
background: #fff;
}
.main-img-box {
width: 347px;
height: auto;
background: #fff;
aspect-ratio: 3/4;
}
.desc-img-wrap {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
.desc-img-box {
width: 212px;
height: 283px;
background: #fff;
object-fit: contain;
aspect-ratio: 3/4;
}
}
.play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 222;
width: 64px;
height: 64px;
background-image: url('@/assets/img/creative-generation-workshop/icon-play.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
transition: background-image 0.3s ease;
}
.play-icon:hover {
background-image: url('@/assets/img/creative-generation-workshop/icon-play-hover.png');
}
.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);
.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);
}
}
}
}
}
}
}

View File

@ -0,0 +1,102 @@
<script lang="jsx">
import TextOverTips from '@/components/text-over-tips';
import { Image, Spin } from '@arco-design/web-vue';
import { exactFormatTime } from '@/utils/tools';
import { getShareWorksList } from '@/api/all/generationWorkshop';
import icon1 from '@/assets/img/error-img.png';
import icon2 from '@/assets/logo.svg';
export default {
setup(props, { emit, expose }) {
const route = useRoute();
const router = useRouter();
const dataSource = ref({});
const loading = ref(false);
const works = computed(() => dataSource.value.works ?? []);
const shareCode = computed(() => route.params.shareCode);
const onClickItem = (item) => {
router.push({
path: `/explore/detail/${shareCode.value}/${item.id}`,
});
};
const getShareWorks = async () => {
try {
loading.value = true;
const { code, data } = await getShareWorksList(shareCode.value);
if (code === 200) {
dataSource.value = data;
}
} finally {
loading.value = false;
}
};
onMounted(() => {
getShareWorks();
});
return () => {
return (
<div class="explore-page">
<header class="page-header">
<div class="content w-full px-24px flex items-center bg-#fff">
<img src={icon2} alt="" width={130} />
</div>
</header>
{loading.value ? (
<Spin spinning={loading.value} class="h-500px w-full flex justify-center items-center" size={60} />
) : (
<section class="page-wrapper flex justify-center">
<div class="explore-container">
<div class="explore-list-wrap pt-24px pb-28px">
<TextOverTips context={`${works.value[0]?.title}${works.value.length}个文件`} class="mb-8px" />
<p class="cts !color-#939499 mb-24px">
{`分享时间:${exactFormatTime(dataSource.value.created_at, 'YYYY-MM-DD HH:mm:ss')} 有效期${
dataSource.value.days
}`}
</p>
<div class="card-container">
{works.value.map((item) => {
return (
<div
class="card-item rounded-8px overflow-hidden"
key={item.id}
onClick={() => onClickItem(item)}
>
<Image
src={item.cover}
width={'100%'}
height={300}
preview={false}
fit="cover"
v-slots={{
error: () => <img src={icon1} class="w-full h-full" />,
}}
/>
<div class="p-16px">
<TextOverTips context={item.title} class=" !lh-24px !text-16px mb-8px bold" />
<p class="cts color-#737478">
{exactFormatTime(item.last_modified_at, 'YYYY年MM月DD日')}修改
</p>
</div>
</div>
);
})}
</div>
</div>
</div>
</section>
)}
</div>
);
};
},
};
</script>
<style lang="scss" scoped>
@import './style.scss';
</style>

View File

@ -0,0 +1,59 @@
.explore-page {
position: relative;
padding-top: $navbar-height;
min-width: 1200px;
background: #fff;
.page-header {
position: fixed;
left: 0;
right: 0;
top: 0;
z-index: 1000;
min-width: 1200px;
.content {
height: $navbar-height;
border-bottom: 1px solid var(--Border-1, #d7d7d9);
}
}
.page-wrapper {
min-height: 500px;
.explore-container {
width: 1200px;
.explore-list-wrap {
.cts {
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
:deep(.overflow-text) {
color: var(--Text-1, #211f24);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
&.bold {
font-family: $font-family-medium;
}
}
.card-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
.card-item {
border: 1px solid var(--Border-1, #d7d7d9);
cursor: pointer;
transition: all 0.3s;
&:hover {
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.15);
border: 1.01px solid var(--Border-1, #d7d7d9);
border-radius: 8.08px;
}
}
}
}
}
}
}

View File

@ -39,7 +39,7 @@ export default {
<span class="mr-8px cts bold">批量审核列表</span>
<span class="mr-8px cts !lh-22px">{`${dataSource.value.length}`}</span>
</div>
<icon-menu-unfold size={16} class="color-##55585F cursor-pointer" onClick={onClose} />
<icon-menu-unfold size={16} class="color-##55585F cursor-pointer hover:color-#6D4CFE" onClick={onClose} />
</div>
<div class="flex-1 overflow-y-auto px-24px">
{dataSource.value.map((item) => (

View File

@ -22,6 +22,7 @@ import 'swiper/css/navigation';
import { Navigation } from 'swiper/modules';
import { FORM_RULES, enumTab, TAB_LIST, RESULT_LIST } from './constants';
import { getImagePreSignedUrl } from '@/api/all/common';
import { EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants';
import icon1 from '@/assets/img/creative-generation-workshop/icon-magic.png';
import icon2 from '@/assets/img/creative-generation-workshop/icon-line.png';
@ -53,6 +54,12 @@ export default {
const modules = [Navigation];
const checkLoading = ref(false);
const tabList = computed(() => {
if (props.modelValue.type === EnumManuscriptType.Image) {
return TAB_LIST;
}
return TAB_LIST.filter((item) => item.value !== enumTab.IMAGE);
});
const isTextTab = computed(() => activeTab.value === enumTab.TEXT);
const onAiReplace = () => {
@ -336,7 +343,7 @@ export default {
))}
</div>
<div class="mb-16px suggestion-box p-12px rounded-8px bg-#F7F8FA flex flex-col">
<div class=" mb-24px relative w-fit">
<div class="mb-24px relative w-fit">
<span class="ai-text">AI 审核建议</span>
<img src={icon2} class="w-80px h-10.8px absolute bottom-1px left-1px" />
</div>
@ -434,7 +441,7 @@ export default {
<div class="flex-2 left-box mr-24px flex flex-col">
<div class="flex-1 mb-12px rounded-8px border-1px pt-8px flex flex-col pb-16px bg-#F7F8FA border-#E6E6E8 border-solid">
<Tabs v-model={activeTab.value} onTabClick={handleTabClick} class="mb-16px">
{TAB_LIST.map((item) => (
{tabList.value.map((item) => (
<TabPane key={item.value} title={item.label}></TabPane>
))}
</Tabs>

View File

@ -17,18 +17,13 @@ export default {
type: Array,
default: () => [],
},
modelValue: {
type: Object,
default: {},
},
selectCardInfo: {
type: Object,
default: {},
},
},
emits: ['update:modelValue', 'cardClick'],
emits: ['cardClick', 'platformChange'],
setup(props, { emit, expose }) {
const selectedPlatform = ref(1);
const modules = [Navigation];
const handleCardClick = (item) => {
// emit('update:modelValue', item);
@ -94,15 +89,15 @@ export default {
<div
key={item.value}
onClick={() => {
selectedPlatform.value = item.value;
emit('platformChange', item.value);
}}
class={`w-100px flex items-center mr-16px py-8px px-12px flex border-1px border-solid border-transparent transition-all
items-center rounded-8px cursor-pointer bg-#F2F3F5 hover:bg-#E6E6E8 ${
selectedPlatform.value === item.value ? '!bg-#F0EDFF !border-#6D4CFE' : ''
props.selectCardInfo.platform === item.value ? '!bg-#F0EDFF !border-#6D4CFE' : ''
}`}
>
<img src={item.icon} alt="" width={20} height={20} class="mr-4px" />
<span class={`cts !color-#211F24 ${selectedPlatform.value === item.value ? 'bold' : ''}`}>
<span class={`cts !color-#211F24 ${props.selectCardInfo.platform === item.value ? 'bold' : ''}`}>
{item.label}
</span>
</div>

View File

@ -64,10 +64,15 @@ export default {
const getWorkAudits = async () => {
const { code, data } = await getWorkAuditsBatchDetail({ ids: workIds.value });
if (code === 200) {
dataSource.value = data ?? [];
remoteDataSource.value = cloneDeep(data ?? []);
selectCardInfo.value = cloneDeep(data?.[0] ?? {});
selectedImageInfo.value = data?.[0].files?.[0] ?? {};
const _data = (data ?? []).map((item) => ({
...item,
platform: item.platform === 0 ? 1 : item.platform,
}));
dataSource.value = _data;
remoteDataSource.value = cloneDeep(_data);
selectCardInfo.value = cloneDeep(_data?.[0] ?? {});
selectedImageInfo.value = cloneDeep(_data?.[0].files?.[0] ?? {});
}
};
@ -77,6 +82,9 @@ export default {
resolve(!isEqual(selectCardInfo.value, _item) && !isSaved.value);
});
};
const onPlatformChange = (platform) => {
selectCardInfo.value.platform = platform;
};
const onExit = async () => {
const isModified = await isSelectCardModified();
@ -165,13 +173,18 @@ export default {
class="check-list-icon"
onClick={() => checkListDrawerRef.value.open(dataSource.value, selectCardInfo.value)}
>
<icon-menu-fold size={16} class="color-#55585F mr-4px" />
<icon-menu-fold size={16} class="color-#55585F mr-4px hover:color-#6D4CFE" />
<span class="cts !color-#211F24">审核列表</span>
</div>
)}
<div class="flex-1 flex flex-col overflow-hidden bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid">
<HeaderCard dataSource={dataSource.value} selectCardInfo={selectCardInfo.value} onCardClick={onCardClick} />
<HeaderCard
dataSource={dataSource.value}
selectCardInfo={selectCardInfo.value}
onCardClick={onCardClick}
onPlatformChange={onPlatformChange}
/>
<section class="flex-1 overflow-hidden">
<ContentCard
ref={contentCardRef}

View File

@ -4,6 +4,7 @@ import CommonSelect from '@/components/common-select';
import { useClipboard } from '@vueuse/core';
import { postShareLinksGenerate } from '@/api/all/generationWorkshop';
import { generateFullUrl } from '@/utils/tools';
const INITIAL_FORM = {
work_ids: [],
@ -44,6 +45,7 @@ export default {
const formRef = ref(null);
const formData = ref(cloneDeep(INITIAL_FORM));
const loading = ref(false);
const router = useRouter();
const { copy } = useClipboard({ source: formData.value.link });
@ -63,9 +65,13 @@ export default {
try {
loading.value = true;
const { code, data } = await postShareLinksGenerate(formData.value);
if (!code) {
if (code === 200) {
onClose();
copy(data.code);
const url = router.resolve({
path: `/explore/list/${data.code}`,
}).href;
copy(generateFullUrl(url));
AMessage.success('复制成功!');
emit('close');
}