first commit

This commit is contained in:
muzi
2025-06-16 14:42:26 +08:00
commit 6f06721506
149 changed files with 56883 additions and 0 deletions

View File

@ -0,0 +1,50 @@
<template>
<div class="m-auto mt-24px max-w-1000px">
<Container title="推荐产品" class="body">
<div class="flex flex-wrap">
<Product
v-for="product in products"
:key="product.id"
class="mt-20px ml-20px"
:product="product"
@refresh="getProductList"
/>
</div>
</Container>
<Container title="成功案例" class="body mt-24px">
<div class="flex flex-wrap">
<Case v-for="item in cases" :key="item.id" class="mt-20px ml-20px" :data="item"></Case>
</div>
</Container>
</div>
</template>
<script setup lang="ts">
import Container from '@/views/components/workplace/modules/container.vue';
import Product from '@/views/components/workplace/modules/product.vue';
import Case from '@/views/components/workplace/modules/case.vue';
import { fetchProductList, fetchSuccessCaseList } from '@/api/all/index';
import { ref, onMounted } from 'vue';
const products = ref([]);
const cases = ref([]);
onMounted(() => {
getProductList();
getSuccessCaseList();
});
const getProductList = async () => {
products.value = await fetchProductList();
};
const getSuccessCaseList = async () => {
cases.value = await fetchSuccessCaseList();
};
</script>
<style scoped lang="less">
.body {
padding-left: 0;
:deep(> .title) {
padding-left: 20px;
}
}
</style>

View File

@ -0,0 +1,190 @@
<template>
<div class="container">
<swiper
:loop="props.data.files.length > 1"
:autoplay="{
delay: 2500,
disableOnInteraction: false,
}"
pagination
:modules="modules"
class="carousel"
>
<swiper-slide v-for="(item, index) in props.data.files" :key="index" class="swiper-slide">
<img v-if="props.data.type === Type.Image" :src="item" alt="" />
<div v-if="props.data.type === Type.Video" class="position-relative">
<video ref="videoRef" :src="item" />
<div class="play flex item-center" @click="playVideo(index)">
<img src="@/assets/play.svg" alt="" />
<span>{{ Math.floor(durationMap[index] / 60) + ':' + Math.floor(durationMap[index] % 60) }}</span>
</div>
</div>
</swiper-slide>
</swiper>
<div class="body">
<img class="logo" :src="props.data.brand_logo" :alt="props.data.brand_name" />
<h1 class="title">{{ props.data.title }}</h1>
<p class="keywords-container">
<span v-for="(item, index) in props.data.keywords" :key="index" class="keyword">{{ item }}</span>
</p>
</div>
<div class="footer flex arco-row-justify-space-around flex-center">
<div v-for="(item, index) in props.data.data" :key="index">
<div class="value">
{{ parseFloat(item.value) }}<span class="unit">{{ item.value.replace(parseFloat(item.value), '') }}</span>
</div>
<div class="label">{{ item.label }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Swiper, SwiperSlide } from 'swiper/vue';
import 'swiper/css';
import 'swiper/css/pagination';
import { Pagination } from 'swiper/modules';
import { ref, watch } from 'vue';
const props = defineProps<{
data: SuccessCase;
}>();
const modules = [Pagination];
const videoRef = ref<HTMLVideoElement[] | null>(null);
const durationMap = ref<number[]>([]);
watch(videoRef, (videos) => {
if (videos) {
for (let i = 0; i < videos.length; i++) {
videos[i].addEventListener('loadedmetadata', () => {
durationMap.value[i] = videos[i]?.duration; // 获取时长并更新到响应式变量
});
videos[i].addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
videos[i].pause(); // 如果没有元素处于全屏状态,则暂停视频
}
});
}
}
});
const playVideo = (index: number) => {
if (videoRef.value) {
videoRef.value[index].requestFullscreen();
videoRef.value[index].play();
}
};
enum Type {
Video = 0,
Image = 1,
}
interface SuccessCase {
id: number;
type: Type;
title: string;
keywords: string[];
files: string[];
brand_name: string;
brand_logo: string;
data: { label: string; value: string }[];
}
</script>
<style scoped lang="less">
.container {
width: 304px;
height: 306px;
border-radius: 8px;
border: 1px solid var(--BG-300, rgba(230, 230, 232, 1));
overflow: hidden;
.carousel {
width: 304px;
height: 140px;
img {
width: 304px;
height: 140px;
}
video {
width: 304px;
height: 140px;
}
.play {
border-radius: 100px;
width: 110px;
height: 48px;
border: 1px solid var(--BG-300, rgba(230, 230, 232, 1));
position: absolute;
top: 46px;
left: 97px;
background-color: #f4f4f6;
img {
width: 34px;
height: 34px;
margin-left: 7px;
margin-right: 12px;
}
span {
font-family: HarmonyOS Sans SC, serif;
font-weight: 700;
font-size: 14px;
line-height: 22px;
color: var(--Text-4, rgba(147, 148, 153, 1));
}
}
}
.body {
padding: 12px 20px;
border-bottom: 1px solid var(--BG-300, rgba(230, 230, 232, 1));
.logo {
height: 14px;
max-width: 100%;
}
.title {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
line-height: 22px;
vertical-align: middle;
margin-top: 8px;
padding: 0;
}
.keywords-container {
margin: 0;
padding: 0;
.keyword {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
line-height: 20px;
color: var(--Text-4, rgba(147, 148, 153, 1));
margin-right: 8px;
}
}
}
.footer {
padding-top: 10px;
.value {
font-family: HarmonyOS Sans SC, serif;
font-weight: 700;
font-size: 20px;
line-height: 28px;
text-align: center;
.unit {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 10px;
line-height: 24px;
margin: 0;
padding: 0;
}
}
.label {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 10px;
line-height: 100%;
text-align: center;
color: var(--Text-4, rgba(147, 148, 153, 1));
}
}
}
</style>

View File

@ -0,0 +1,29 @@
<template>
<div class="container">
<h1 class="title">{{ props.title }}</h1>
<div>
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
title: string;
}>();
</script>
<style scoped lang="less">
.container {
border: 1px solid var(--BG-300, rgba(230, 230, 232, 1));
background: var(--BG-white, rgba(255, 255, 255, 1));
padding: 16px 24px 20px 24px;
}
.title {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 18px;
line-height: 24px;
vertical-align: middle;
margin-bottom: 0px;
}
</style>

View File

@ -0,0 +1,249 @@
<template>
<div class="container">
<div class="flex arco-row-justify-space-between flex-center">
<img class="avatar" :src="props.product.image" :alt="props.product.name" />
<a-tag v-if="props.product.status === Status.Enable" class="status status-enable">已开通</a-tag>
<a-tag v-if="props.product.status === Status.Disable" class="status status-disable">未开通</a-tag>
<a-tag v-if="props.product.status === Status.EXPIRED" class="status status-expired">已到期</a-tag>
<a-tag v-if="props.product.status === Status.TRIAL_ENDS" class="status status-expired">试用结束</a-tag>
<a-countdown
v-if="props.product.status === Status.ON_TRIAL"
class="status-on-trill"
title="试用中"
:value="1000 * (props.product.expired_at ?? 0)"
:now="now()"
format="D天H时m分s秒"
/>
</div>
<div class="body">
<h1 class="title">{{ props.product.name }}</h1>
<p class="desc">
{{ props.product.desc }}
</p>
</div>
<div class="footer flex arco-row-justify-start flex-center">
<a-button
v-if="props.product.status === Status.Enable || props.product.status === Status.ON_TRIAL"
class="primary-button"
type="primary"
@click="gotoModule(props.product.menu_id)"
>
进入模块
</a-button>
<a-button
v-if="props.product.status === Status.TRIAL_ENDS || props.product.status === Status.EXPIRED"
class="primary-button"
type="primary"
@click="visible = true"
>
立即购买
</a-button>
<a-button
v-if="props.product.status === Status.ON_TRIAL"
class="outline-button"
type="outline"
@click="visible = true"
>
升级购买
</a-button>
<a-button
v-if="props.product.status === Status.TRIAL_ENDS || props.product.status === Status.EXPIRED"
class="outline-button"
type="outline"
@click="visible = true"
>
联系客服
</a-button>
<a-popconfirm
focusLock
title="试用产品"
content="确定试用该产品吗?"
@ok="handleTrial(props.product.id)"
>
<a-button v-if="props.product.status === Status.Disable" class="outline-button" type="outline">
免费试用7天
</a-button>
</a-popconfirm>
</div>
<a-modal v-model:visible="visible">
<template #title>
扫描下面二维码联系客户
</template>
<div class="text-center">
<img width="200" src="@/assets/customer-service.svg" alt="" />
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { now } from '@vueuse/core';
import { trialProduct } from '@/api/all';
import { useRouter } from 'vue-router';
const props = defineProps<{
product: Product;
}>();
const emit = defineEmits(['refresh']);
const visible = ref(false);
const router = useRouter();
enum Status {
Disable = 0, // 禁用
Enable = 1, // 启用
ON_TRIAL = 2, // 试用中
EXPIRED = 3, // 已过期
TRIAL_ENDS = 4, // 试用结束
}
interface Product {
id: number;
status: Status;
name: string;
image: string;
desc: string;
menu_id: number;
expired_at?: number;
}
const handleTrial = async (id: any) => {
await trialProduct(id);
AMessage.success('试用成功!');
emit('refresh');
};
const gotoModule = (menuId: number) => {
router.push({ name: 'dataEngine' });
};
</script>
<style scoped lang="less">
.container {
width: 304px;
height: 220px;
border-radius: 8px;
border: 1px solid rgba(230, 230, 232, 1);
padding: 20px;
.avatar {
width: 40px;
height: 40px;
}
.status {
height: 20px;
border-radius: 4px;
padding-right: 8px;
padding-left: 8px;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
line-height: 20px;
}
.status-enable {
background: rgba(235, 247, 242, 1);
color: rgba(27, 174, 113, 1);
}
.status-disable {
background: rgba(242, 243, 245, 1);
color: rgba(147, 148, 153, 1);
}
.status-expired {
background: rgba(255, 231, 228, 1);
color: rgba(197, 60, 39, 1);
}
.status-expired {
background: rgba(255, 231, 228, 1);
color: rgba(197, 60, 39, 1);
}
.status-on-trill {
padding-left: 8px;
padding-right: 8px;
height: 36px;
border-radius: 4px;
background: rgba(255, 245, 222, 1);
:deep(.arco-statistic-title) {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
color: var(--Functional-Warning-7, rgba(204, 139, 0, 1));
text-align: center;
margin: 0;
padding: 0;
}
:deep(.arco-statistic-value) {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 10px;
color: var(--Functional-Warning-7, rgba(204, 139, 0, 1));
text-align: center;
margin: 0;
padding: 0;
}
}
.body {
height: 88px;
margin-top: 12px;
.title {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 16px;
line-height: 24px;
vertical-align: middle;
margin: 0;
}
.desc {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
line-height: 20px;
margin: 0;
}
}
.footer {
padding-top: 16px;
.primary-button {
height: 24px;
border-radius: 4px;
gap: 8px;
padding: 2px 12px;
background-color: rgba(109, 76, 254, 1) !important;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
line-height: 20px;
text-align: right;
vertical-align: middle;
color: rgba(255, 255, 255, 1);
margin-right: 8px;
}
.outline-button {
height: 24px;
border-radius: 4px;
gap: 8px;
padding: 2px 12px;
border: 1px solid var(--Brand-Brand-6, rgba(109, 76, 254, 1));
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
line-height: 20px;
text-align: right;
vertical-align: middle;
color: rgba(109, 76, 254, 1);
margin-right: 8px;
}
}
}
</style>