first commit
This commit is contained in:
184
src/views/components/dataEngine/hotCloud.vue
Normal file
184
src/views/components/dataEngine/hotCloud.vue
Normal file
@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<view>
|
||||
<topHeader ref="topHeaderRef"></topHeader>
|
||||
<a-space direction="vertical" style="background-color: #fff; width: 100%; padding: 24px; margin: 24px 0">
|
||||
<a-space align="center">
|
||||
<span>行业词云</span>
|
||||
<a-popover position="tl">
|
||||
<a-button type="primary" class="pop-btn">
|
||||
<template #icon>
|
||||
<icon-question-circle />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<p>基于xxx获取数据xxx,一段文字描述该数据的获取方式和来源等xxx</p>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
<div class="multi-row-tag-cloud">
|
||||
<!-- 动态生成多行标签 -->
|
||||
<div
|
||||
v-for="(row, rowIndex) in tagRows"
|
||||
:key="rowIndex"
|
||||
class="tag-row"
|
||||
:style="{ justifyContent: row.align || 'center' }"
|
||||
>
|
||||
<a-tag
|
||||
v-for="(tag, tagIndex) in row.tags"
|
||||
:key="tagIndex"
|
||||
:style="{
|
||||
fontSize: `${getFontSize(rowIndex, tagIndex)}px`,
|
||||
lineHeight: `${getLineHeight(rowIndex, tagIndex) + 10}px`,
|
||||
color: '#6d4cfe',
|
||||
backgroundColor: '#F0EDFF',
|
||||
margin: '12px',
|
||||
transition: 'all 0.3s',
|
||||
paddingLeft: `${getPaddingLeft(rowIndex, tagIndex)}px`,
|
||||
paddingRight: `${getPaddingLeft(rowIndex, tagIndex)}px`,
|
||||
paddingTop: `${getPadding(rowIndex, tagIndex)}px`,
|
||||
paddingBottom: `${getPadding(rowIndex, tagIndex)}px`,
|
||||
borderRadius: '100px',
|
||||
borderRadius: '100px',
|
||||
}"
|
||||
@mouseenter="hoverTag = tag"
|
||||
@mouseleave="hoverTag = null"
|
||||
>
|
||||
<a-space>
|
||||
<a-tooltip :content="`性价比:${Number(tag.rate * 100)}%`" position="tl">
|
||||
<a-space>{{ tag.term }}</a-space>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</a-space>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import topHeader from './topHeader.vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { fetchindustryTerms } from '@/api/all/index';
|
||||
|
||||
const topHeaderRef = ref();
|
||||
// 从topHeader获取统一的状态
|
||||
const selectedIndustry = computed(() => topHeaderRef.value?.selectedIndustry);
|
||||
const selectedSubCategory = computed(() => topHeaderRef.value?.selectedSubCategory);
|
||||
const selectedTimePeriod = computed(() => topHeaderRef.value?.selectedTimePeriod);
|
||||
|
||||
const getIndustryTerms = async () => {
|
||||
const params = {
|
||||
industry_id: selectedIndustry.value,
|
||||
time_dimension: selectedTimePeriod.value,
|
||||
};
|
||||
const res = await fetchindustryTerms(params);
|
||||
// 这里需要根据API返回的数据结构处理成tagRows需要的格式
|
||||
tagRows.value = processTagData(res);
|
||||
};
|
||||
|
||||
// 标签数据(按行分组)
|
||||
const tagRows = ref();
|
||||
const hoverTag = ref(null);
|
||||
|
||||
// 根据行列位置计算字体大小(中间最大)
|
||||
const getFontSize = (rowIndex, tagIndex) => {
|
||||
const centerRow = Math.floor(tagRows.value.length / 2);
|
||||
const distance = Math.abs(rowIndex - centerRow);
|
||||
return 38 - distance * 8 + (tagIndex % 2) * 5; // 基础24px,行距影响4px,微调差异2px
|
||||
};
|
||||
|
||||
const getLineHeight = (rowIndex, tagIndex) => {
|
||||
const centerRow = Math.floor(tagRows.value.length / 2);
|
||||
const distance = Math.abs(rowIndex - centerRow);
|
||||
return 48 - distance * 8 + (tagIndex % 2) * 5; // 基础24px,行距影响4px,微调差异2px
|
||||
};
|
||||
|
||||
const getPaddingLeft = (rowIndex, tagIndex) => {
|
||||
const centerRow = Math.floor(tagRows.value.length / 2);
|
||||
const distance = Math.abs(rowIndex - centerRow);
|
||||
return 30 - distance * 8 + (tagIndex % 2) * 5; // 基础24px,行距影响4px,微调差异2px
|
||||
};
|
||||
|
||||
const getPadding = (rowIndex, tagIndex) => {
|
||||
const centerRow = Math.floor(tagRows.value.length / 2);
|
||||
const distance = Math.abs(rowIndex - centerRow);
|
||||
return 28 - distance * 8 + (tagIndex % 2) * 5; // 基础24px,行距影响4px,微调差异2px
|
||||
};
|
||||
|
||||
// 处理API返回数据为tagRows格式
|
||||
const processTagData = (apiData) => {
|
||||
const totalGroups = 7; // 总组数
|
||||
const middleIndex = Math.floor(totalGroups / 2); // 中间位置(索引3)
|
||||
const chunkSize = Math.ceil(apiData.length / totalGroups); // 每组大小
|
||||
const arr = [];
|
||||
|
||||
// 1. 先按顺序分组
|
||||
const tempGroups = [];
|
||||
for (let i = 0; i < totalGroups; i++) {
|
||||
const start = i * chunkSize;
|
||||
const end = start + chunkSize;
|
||||
tempGroups.push(apiData.slice(start, end));
|
||||
}
|
||||
|
||||
// 2. 从中间开始,左右交替插入
|
||||
for (let i = 0; i < totalGroups; i++) {
|
||||
// 计算当前组应该插入的位置
|
||||
const insertPos = i % 2 === 0 ? middleIndex + i / 2 : middleIndex - Math.ceil(i / 2);
|
||||
|
||||
// 确保不越界
|
||||
if (insertPos >= 0 && insertPos < totalGroups) {
|
||||
arr[insertPos] = { tags: tempGroups[i], align: 'center' };
|
||||
}
|
||||
}
|
||||
|
||||
return arr.filter(Boolean); // 移除可能的空项
|
||||
};
|
||||
|
||||
// 监听筛选条件变化
|
||||
watch([selectedIndustry, selectedTimePeriod], () => {
|
||||
getIndustryTerms();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getIndustryTerms();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义样式 */
|
||||
:deep(.arco-table-th) {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
|
||||
:deep(.arco-table-tr):hover {
|
||||
background-color: var(--color-fill-1);
|
||||
}
|
||||
:deep(.arco-btn-outline) {
|
||||
color: #6d4cfe !important;
|
||||
border-color: #6d4cfe !important;
|
||||
}
|
||||
:deep(.arco-modal-body) {
|
||||
padding: 0px;
|
||||
}
|
||||
.multi-row-tag-cloud {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 悬停放大效果 */
|
||||
a-tag:hover {
|
||||
transform: scale(1.1);
|
||||
z-index: 1;
|
||||
}
|
||||
.pop-btn {
|
||||
background: #fff !important;
|
||||
border-color: #fff !important;
|
||||
color: #737478 !important;
|
||||
margin-left: -5px;
|
||||
}
|
||||
</style>
|
||||
224
src/views/components/dataEngine/hotTranslation.vue
Normal file
224
src/views/components/dataEngine/hotTranslation.vue
Normal file
@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<view>
|
||||
<topHeader ref="topHeaderRef"></topHeader>
|
||||
<!-- tabel -->
|
||||
<a-space direction="vertical" style="background-color: #fff; width: 100%; padding: 24px; margin-bottom: 24px">
|
||||
<a-space align="center">
|
||||
<span>行业热门话题洞察</span>
|
||||
<a-popover position="tl">
|
||||
<a-button type="primary" class="pop-btn">
|
||||
<template #icon>
|
||||
<icon-question-circle />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<p>基于xxx获取数据xxx,一段文字描述该数据的获取方式和来源等xxx</p>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
<a-table :data="dataList">
|
||||
<template #columns>
|
||||
<a-table-column title="排名" data-index="rank">
|
||||
<template #cell="{ record }">
|
||||
<img v-if="record.rank == 1" :src="topImages[0]" style="width: 25px; height: 17px" />
|
||||
<img v-else-if="record.rank == 2" :src="topImages[1]" style="width: 25px; height: 17px" />
|
||||
<img v-else-if="record.rank == 3" :src="topImages[2]" style="width: 25px; height: 17px" />
|
||||
<span v-else>{{ record.rank }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="话题名称" data-index="name" />
|
||||
<a-table-column title="关键词" data-index="keywords">
|
||||
<template #cell="{ record }">
|
||||
<a-tag v-for="item in record.keywords" :key="item" style="margin-right: 5px">{{ item }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="热度" data-index="heatLevel">
|
||||
<template #cell="{ record }">
|
||||
<img v-for="i in record.hot" :key="i" :src="starImages[i - 1]" style="width: 16px; height: 16px" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="情感" data-index="sentiment">
|
||||
<template #cell="{ record }">
|
||||
<img
|
||||
v-if="record.felling == '2'"
|
||||
src="@/assets/img/hottranslation/good.png"
|
||||
style="width: 16px; height: 16px"
|
||||
/>
|
||||
<img
|
||||
v-else-if="record.felling == '1'"
|
||||
src="@/assets/img/hottranslation/normal.png"
|
||||
style="width: 16px; height: 16px"
|
||||
/>
|
||||
<img
|
||||
v-else-if="record.felling == '0'"
|
||||
src="@/assets/img/hottranslation/poor.png"
|
||||
style="width: 16px; height: 16px"
|
||||
/>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" data-index="optional">
|
||||
<template #cell="{ record }">
|
||||
<a-button type="outline" @click="gotoDetail(record)">详情</a-button>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
<template #rank="{ record }">
|
||||
<a-tag color="blue" v-if="record.rank == 1">1</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-space>
|
||||
<!-- modal -->
|
||||
<a-modal :visible="visible" @ok="handleOk" @cancel="handleCancel" unmountOnClose>
|
||||
<template #title>
|
||||
<span style="text-align: left; width: 100%">行业热门话题洞察</span>
|
||||
</template>
|
||||
<div>
|
||||
<a-space direction="vertical">
|
||||
<a-space>
|
||||
<span style="margin-right: 16px">话题名称</span>
|
||||
<span>{{ topicInfo.name }}</span>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<span style="margin-right: 16px">话题简介</span>
|
||||
<span>{{ topicInfo.intro }}</span>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<span style="margin-right: 16px">关键词</span>
|
||||
<a-tag v-for="item in topicInfo.keywords" :key="item" style="margin-right: 5px">{{ item }}</a-tag>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<span style="margin-right: 16px">热度指数</span>
|
||||
<img v-for="i in topicInfo.hot" :key="i" :src="starImages[i - 1]" style="width: 16px; height: 16px" />
|
||||
</a-space>
|
||||
<a-space>
|
||||
<span style="margin-right: 16px">情感指数</span>
|
||||
<img
|
||||
v-if="topicInfo.felling == '2'"
|
||||
src="@/assets/img/hottranslation/good.png"
|
||||
style="width: 16px; height: 16px"
|
||||
/>
|
||||
<img
|
||||
v-else-if="topicInfo.felling == '1'"
|
||||
src="@/assets/img/hottranslation/normal.png"
|
||||
style="width: 16px; height: 16px"
|
||||
/>
|
||||
<img
|
||||
v-else-if="topicInfo.felling == '0'"
|
||||
src="@/assets/img/hottranslation/poor.png"
|
||||
style="width: 16px; height: 16px"
|
||||
/>
|
||||
</a-space>
|
||||
<a-space direction="top">
|
||||
<span style="margin-right: 16px; width: 60px; font-size: 12px">原始来源 </span>
|
||||
<a-space direction="vertical" style="margin-left: 15px">
|
||||
<a-space v-for="item in topicInfo.industry_topic_sources" :key="item">
|
||||
<a-link style="background-color: initial" :href="item.link">{{ item.title }}</a-link>
|
||||
<img src="@/assets/img/hottranslation/xhs.png" style="width: 16px; height: 16px" />
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-modal>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import topHeader from './topHeader.vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { fetchIndustriesTree, fetchIndustryTopics, fetchIndustryTopicDetail } from '@/api/all/index';
|
||||
import star1 from '@/assets/img/hottranslation/star-fill1.png';
|
||||
import star2 from '@/assets/img/hottranslation/star-fill2.png';
|
||||
import star3 from '@/assets/img/hottranslation/star-fill3.png';
|
||||
import star4 from '@/assets/img/hottranslation/star-fill4.png';
|
||||
import star5 from '@/assets/img/hottranslation/star-fill5.png';
|
||||
import top1 from '@/assets/img/captcha/top1.svg';
|
||||
import top2 from '@/assets/img/captcha/top2.svg';
|
||||
import top3 from '@/assets/img/captcha/top3.svg';
|
||||
|
||||
const starImages = [star1, star2, star3, star4, star5];
|
||||
const topImages = [top1, top2, top3];
|
||||
// 行业大类
|
||||
const industriesTree = ref([]);
|
||||
|
||||
// 行业热门话题洞察
|
||||
const dataList = ref([]);
|
||||
// 显示详情
|
||||
const visible = ref(false);
|
||||
const topicInfo = ref({});
|
||||
const topHeaderRef = ref();
|
||||
// 从topHeader获取统一的状态
|
||||
const selectedIndustry = computed(() => topHeaderRef.value?.selectedIndustry);
|
||||
const selectedSubCategory = computed(() => topHeaderRef.value?.selectedSubCategory);
|
||||
const selectedTimePeriod = computed(() => topHeaderRef.value?.selectedTimePeriod);
|
||||
|
||||
// 监听筛选条件变化
|
||||
watch([selectedIndustry, selectedTimePeriod], () => {
|
||||
getIndustryTopics();
|
||||
});
|
||||
onMounted(() => {
|
||||
getIndustriesTree();
|
||||
});
|
||||
// 获取行业大类数据
|
||||
const getIndustriesTree = async () => {
|
||||
const res = await fetchIndustriesTree();
|
||||
industriesTree.value = res;
|
||||
selectedIndustry.value = res[0].id;
|
||||
getIndustryTopics();
|
||||
};
|
||||
|
||||
// 行业热门话题
|
||||
const getIndustryTopics = async () => {
|
||||
let parms = {
|
||||
industry_id: selectedIndustry.value,
|
||||
time_dimension: selectedTimePeriod.value,
|
||||
};
|
||||
const res = await fetchIndustryTopics(parms);
|
||||
console.log(res);
|
||||
dataList.value = res;
|
||||
};
|
||||
|
||||
// 详情
|
||||
const gotoDetail = async (record) => {
|
||||
console.log(record);
|
||||
const res = await fetchIndustryTopicDetail(record.id);
|
||||
console.log(res);
|
||||
visible.value = true;
|
||||
topicInfo.value = res;
|
||||
};
|
||||
|
||||
// 弹窗的取消
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 弹窗的确定
|
||||
const handleOk = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义样式 */
|
||||
:deep(.arco-table-th) {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
|
||||
:deep(.arco-table-tr):hover {
|
||||
background-color: var(--color-fill-1);
|
||||
}
|
||||
:deep(.arco-btn-outline) {
|
||||
color: #6d4cfe !important;
|
||||
border-color: #6d4cfe !important;
|
||||
}
|
||||
:deep(.arco-modal-body) {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.pop-btn {
|
||||
background: #fff !important;
|
||||
border-color: #fff !important;
|
||||
color: #737478 !important;
|
||||
margin-left: -5px;
|
||||
}
|
||||
</style>
|
||||
165
src/views/components/dataEngine/keyBrandMovement.vue
Normal file
165
src/views/components/dataEngine/keyBrandMovement.vue
Normal file
@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<view>
|
||||
<topHeader ref="topHeaderRef"></topHeader>
|
||||
<!-- 重点品牌列表 -->
|
||||
<a-space direction="vertical" style="background-color: #fff; width: 100%; padding: 24px; margin: 24px 0">
|
||||
<a-space align="center">
|
||||
<span>重点品牌列表 </span>
|
||||
<a-popover position="tl">
|
||||
<a-button type="primary" class="pop-btn">
|
||||
<template #icon>
|
||||
<icon-question-circle />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<p>基于xxx获取数据xxx,一段文字描述该数据的获取方式和来源等xxx</p>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
<a-table :data="dataList" :pagination="false" style="font-size: 12px">
|
||||
<template #columns>
|
||||
<a-table-column title="排名" data-index="rank">
|
||||
<template #cell="{ record }">
|
||||
<img v-if="record.rank == 1" :src="topImages[0]" style="width: 25px; height: 17px" />
|
||||
<img v-else-if="record.rank == 2" :src="topImages[1]" style="width: 25px; height: 17px" />
|
||||
<img v-else-if="record.rank == 3" :src="topImages[2]" style="width: 25px; height: 17px" />
|
||||
<span v-else>{{ record.rank }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="品牌名称" data-index="name" />
|
||||
<a-table-column title="热度指数" data-index="keywords">
|
||||
<template #cell="{ record }">
|
||||
<img v-for="i in record.hot" :key="i" :src="starImages[i - 1]" style="width: 16px; height: 16px" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="变化幅度" data-index="frequency">
|
||||
<template #cell="{ record }">
|
||||
<a-statistic
|
||||
style="font-size: 14px"
|
||||
v-if="record.trend > 0"
|
||||
:value="record.trend * 100"
|
||||
:value-style="{ color: '#F64B31' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-arrow-rise />
|
||||
</template>
|
||||
<template #suffix>%</template>
|
||||
</a-statistic>
|
||||
<a-statistic
|
||||
v-else
|
||||
style="font-size: 14px"
|
||||
:value="record.trend * 100"
|
||||
:value-style="{ color: '#25C883' }"
|
||||
></a-statistic>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="占总声量比例" data-index="content">
|
||||
<template #cell="{ record }"> <a-statistic :value="record.volume_rate * 100" />% </template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-space>
|
||||
<!-- 舆情 & 敏感动态-->
|
||||
<a-space direction="vertical" style="background-color: #fff; width: 100%; padding: 24px; margin: 24px 0">
|
||||
<a-space align="center">
|
||||
<span>舆情 & 敏感动态 </span>
|
||||
<a-popover position="tl">
|
||||
<a-button type="primary" class="pop-btn">
|
||||
<template #icon>
|
||||
<icon-question-circle />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<p>基于xxx获取数据xxx,一段文字描述该数据的获取方式和来源等xxx</p>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
<a-table :data="otherList" :pagination="false" style="font-size: 12px">
|
||||
<template #columns>
|
||||
<a-table-column title="品牌" data-index="brand" />
|
||||
<a-table-column title="事件标题" data-index="title" />
|
||||
<a-table-column title="事件详情" data-index="content" />
|
||||
</template>
|
||||
</a-table>
|
||||
</a-space>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import topHeader from './topHeader.vue';
|
||||
import { fetchFocusBrandsList, fetchEventDynamicsList } from '@/api/all/index';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import star1 from '@/assets/img/hottranslation/star-fill1.png';
|
||||
import star2 from '@/assets/img/hottranslation/star-fill2.png';
|
||||
import star3 from '@/assets/img/hottranslation/star-fill3.png';
|
||||
import star4 from '@/assets/img/hottranslation/star-fill4.png';
|
||||
import star5 from '@/assets/img/hottranslation/star-fill5.png';
|
||||
import top1 from '@/assets/img/captcha/top1.svg';
|
||||
import top2 from '@/assets/img/captcha/top2.svg';
|
||||
import top3 from '@/assets/img/captcha/top3.svg';
|
||||
const topImages = [top1, top2, top3];
|
||||
const starImages = [star1, star2, star3, star4, star5];
|
||||
const topHeaderRef = ref();
|
||||
// 从topHeader获取统一的状态
|
||||
const selectedIndustry = computed(() => topHeaderRef.value?.selectedIndustry);
|
||||
const selectedSubCategory = computed(() => topHeaderRef.value?.selectedSubCategory);
|
||||
const selectedTimePeriod = computed(() => topHeaderRef.value?.selectedTimePeriod);
|
||||
const dataList = ref([]);
|
||||
const otherList = ref([]);
|
||||
|
||||
const getFocusBrandsList = async () => {
|
||||
const params = {
|
||||
industry_id: selectedIndustry.value,
|
||||
time_dimension: selectedTimePeriod.value,
|
||||
};
|
||||
const res = await fetchFocusBrandsList(params);
|
||||
// 这里需要根据API返回的数据结构处理成tagRows需要的格式
|
||||
dataList.value = res;
|
||||
};
|
||||
|
||||
const getEventDynamicsList = async () => {
|
||||
const params = {
|
||||
industry_id: selectedIndustry.value,
|
||||
time_dimension: selectedTimePeriod.value,
|
||||
};
|
||||
const res = await fetchEventDynamicsList(params);
|
||||
// 这里需要根据API返回的数据结构处理成tagRows需要的格式
|
||||
otherList.value = res;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getFocusBrandsList();
|
||||
getEventDynamicsList();
|
||||
});
|
||||
// 监听筛选条件变化
|
||||
watch([selectedIndustry, selectedTimePeriod], () => {
|
||||
getFocusBrandsList();
|
||||
getEventDynamicsList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义样式 */
|
||||
:deep(.arco-table-th) {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
|
||||
:deep(.arco-table-tr):hover {
|
||||
background-color: var(--color-fill-1);
|
||||
}
|
||||
:deep(.arco-statistic-content .arco-statistic-value-integer) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pop-btn {
|
||||
background: #fff !important;
|
||||
border-color: #fff !important;
|
||||
color: #737478 !important;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
:deep(.arco-btn-outline) {
|
||||
color: #6d4cfe !important;
|
||||
border-color: #6d4cfe !important;
|
||||
}
|
||||
</style>
|
||||
359
src/views/components/dataEngine/keyWord.vue
Normal file
359
src/views/components/dataEngine/keyWord.vue
Normal file
@ -0,0 +1,359 @@
|
||||
<template>
|
||||
<view>
|
||||
<topHeader ref="topHeaderRef"></topHeader>
|
||||
<!-- 关键词热度榜 -->
|
||||
<a-space direction="vertical" style="background-color: #fff; width: 100%; padding: 24px; margin: 24px 0">
|
||||
<a-space align="center">
|
||||
<span>关键词热度榜</span>
|
||||
<a-popover position="tl">
|
||||
<a-button type="primary" class="pop-btn">
|
||||
<template #icon>
|
||||
<icon-question-circle />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<p>基于xxx获取数据xxx,一段文字描述该数据的获取方式和来源等xxx</p>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
<a-table :data="dataList" :pagination="false">
|
||||
<template #columns>
|
||||
<a-table-column title="排名" data-index="rank">
|
||||
<template #cell="{ record }">
|
||||
<img v-if="record.rank == 1" :src="topImages[0]" style="width: 25px; height: 17px" />
|
||||
<img v-else-if="record.rank == 2" :src="topImages[1]" style="width: 25px; height: 17px" />
|
||||
<img v-else-if="record.rank == 3" :src="topImages[2]" style="width: 25px; height: 17px" />
|
||||
|
||||
<span v-else>{{ record.rank }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="关键词名称" data-index="name" />
|
||||
<a-table-column title="热度指数" data-index="heatLevel">
|
||||
<template #cell="{ record }">
|
||||
<img v-for="i in record.hot" :key="i" :src="starImages[i - 1]" style="width: 16px; height: 16px" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="变化幅度" data-index="heatLevel">
|
||||
<template #cell="{ record }">
|
||||
<a-statistic
|
||||
style="font-size: 14px"
|
||||
v-if="record.trend > 0"
|
||||
:value="record.trend * 100"
|
||||
:value-style="{ color: '#F64B31' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-arrow-rise />
|
||||
</template>
|
||||
<template #suffix>%</template>
|
||||
</a-statistic>
|
||||
<a-statistic
|
||||
v-else
|
||||
style="font-size: 14px"
|
||||
:value="record.trend * 100"
|
||||
:value-style="{ color: '#25C883' }"
|
||||
></a-statistic>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="情感倾向" data-index="sentiment">
|
||||
<template #cell="{ record }">
|
||||
<img
|
||||
v-if="record.felling == '2'"
|
||||
src="@/assets/img/hottranslation/good.png"
|
||||
style="width: 16px; height: 16px"
|
||||
/>
|
||||
<img
|
||||
v-else-if="record.felling == '1'"
|
||||
src="@/assets/img/hottranslation/normal.png"
|
||||
style="width: 16px; height: 16px"
|
||||
/>
|
||||
<img
|
||||
v-else-if="record.felling == '0'"
|
||||
src="@/assets/img/hottranslation/poor.png"
|
||||
style="width: 16px; height: 16px"
|
||||
/>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-space>
|
||||
<!-- 行业情绪 -->
|
||||
<a-space direction="vertical" style="background-color: #fff; width: 100%; padding: 24px; margin: 24px 0">
|
||||
<a-space align="center">
|
||||
<span>行业情绪</span>
|
||||
<a-popover position="tl">
|
||||
<a-button type="primary" class="pop-btn">
|
||||
<template #icon>
|
||||
<icon-question-circle />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<p>基于xxx获取数据xxx,一段文字描述该数据的获取方式和来源等xxx</p>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
<a-space align="center">
|
||||
<a-space direction="vertical">
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 400px; height: 400px"></div>
|
||||
</template>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<a-space>
|
||||
<div id="container" style="height: 180px; width: 180px"></div>
|
||||
<a-space direction="vertical" style="font-size: 14px" v-if="fellingRate.length > 0">
|
||||
<a-space>
|
||||
<span style="width: 8px; height: 8px; background-color: #25c883; border-radius: 50%"></span>
|
||||
<span>正面情绪 </span>
|
||||
<span style="width: 40px">{{ fellingRate[0] * 100 }}%</span>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<span style="width: 8px; height: 8px; background-color: #f64b31; border-radius: 50%"></span>
|
||||
<span>负面情绪 </span>
|
||||
<span style="width: 40px">{{ fellingRate[1] * 100 }}%</span>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-space>
|
||||
<a-table :pagination="false" :span-method="dataSpanMethod" :data="rowData" style="margin-left: 40px">
|
||||
<template #columns>
|
||||
<a-table-column title="情绪分布">
|
||||
<template #cell="{ record }">
|
||||
<a-space v-if="record.felling == '2'">
|
||||
<img src="@/assets/img/hottranslation/good.png" style="width: 16px; height: 16px" />
|
||||
<a-space>正面情绪</a-space>
|
||||
</a-space>
|
||||
<a-space v-else-if="record.felling == '1'">
|
||||
<img src="@/assets/img/hottranslation/normal.png" style="width: 16px; height: 16px" />
|
||||
<a-space>中性情绪</a-space>
|
||||
</a-space>
|
||||
<a-space v-else-if="record.felling == '0'">
|
||||
<img src="@/assets/img/hottranslation/poor.png" style="width: 16px; height: 16px" />
|
||||
<a-space>负面情绪</a-space>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="主要观点" data-index="content" />
|
||||
</template>
|
||||
</a-table>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-space>
|
||||
<!-- 新兴关键词 -->
|
||||
<a-space direction="vertical" style="background-color: #fff; width: 100%; padding: 24px; margin: 24px 0">
|
||||
<a-space align="center">
|
||||
<span>新兴关键词 </span>
|
||||
<a-popover position="tl">
|
||||
<a-button type="primary" class="pop-btn">
|
||||
<template #icon>
|
||||
<icon-question-circle />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<p>基于xxx获取数据xxx,一段文字描述该数据的获取方式和来源等xxx</p>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
<a-table :data="keywordList" :pagination="false">
|
||||
<template #columns>
|
||||
<a-table-column title="排名" data-index="rank" />
|
||||
<a-table-column title="新兴关键词名称" data-index="name" />
|
||||
<a-table-column title="首次大规模出现" data-index="first_appeared_at">
|
||||
<template #cell="{ record }">
|
||||
<div>{{ formatTimestamp(record.first_appeared_at) }}</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="当前热度指数" data-index="heatLevel">
|
||||
<template #cell="{ record }">
|
||||
<img v-for="i in record.hot" :key="i" :src="starImages[i - 1]" style="width: 16px; height: 16px" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="变化幅度" data-index="trend">
|
||||
<template #cell="{ record }">
|
||||
<a-statistic
|
||||
style="font-size: 14px"
|
||||
v-if="record.trend > 0"
|
||||
:value="record.trend * 100"
|
||||
:value-style="{ color: '#F64B31' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-arrow-rise />
|
||||
</template>
|
||||
<template #suffix>%</template>
|
||||
</a-statistic>
|
||||
<a-statistic
|
||||
v-else
|
||||
style="font-size: 14px"
|
||||
:value="record.trend * 100"
|
||||
:value-style="{ color: '#25C883' }"
|
||||
></a-statistic>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-space>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import topHeader from './topHeader.vue';
|
||||
import { fetchKeywordTrendsList, fetchIndustryEmotions, fetchNewKeywordList } from '@/api/all/index';
|
||||
import { ref, onMounted, onBeforeUnmount, watchEffect, computed } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import star1 from '@/assets/img/hottranslation/star-fill1.png';
|
||||
import star2 from '@/assets/img/hottranslation/star-fill2.png';
|
||||
import star3 from '@/assets/img/hottranslation/star-fill3.png';
|
||||
import star4 from '@/assets/img/hottranslation/star-fill4.png';
|
||||
import star5 from '@/assets/img/hottranslation/star-fill5.png';
|
||||
import top1 from '@/assets/img/captcha/top1.svg';
|
||||
import top2 from '@/assets/img/captcha/top2.svg';
|
||||
import top3 from '@/assets/img/captcha/top3.svg';
|
||||
|
||||
const starImages = [star1, star2, star3, star4, star5];
|
||||
const topImages = [top1, top2, top3];
|
||||
|
||||
const chartRef = (ref < HTMLElement) | (null > null);
|
||||
const topHeaderRef = ref();
|
||||
// 从topHeader获取统一的状态
|
||||
const selectedIndustry = computed(() => topHeaderRef.value?.selectedIndustry);
|
||||
const selectedSubCategory = computed(() => topHeaderRef.value?.selectedSubCategory);
|
||||
const selectedTimePeriod = computed(() => topHeaderRef.value?.selectedTimePeriod);
|
||||
const dataList = ref([]);
|
||||
const rowData = ref([]);
|
||||
const keywordList = ref([]);
|
||||
const fellingRate = ref([]);
|
||||
const getIndustryEmotions = async () => {
|
||||
const params = {
|
||||
industry_id: selectedIndustry.value,
|
||||
time_dimension: selectedTimePeriod.value,
|
||||
};
|
||||
const res = await fetchIndustryEmotions(params);
|
||||
fellingRate.value.push(res['good_felling_rate']);
|
||||
fellingRate.value.push(res['bad_felling_rate']);
|
||||
|
||||
drawChart();
|
||||
rowData.value = res['industry_emotion_view_points'];
|
||||
let items = groupedData();
|
||||
console.log('行业情绪', items);
|
||||
};
|
||||
|
||||
const groupedData = () => {
|
||||
const groups = {
|
||||
negative: { name: '负面', items: [], color: '#F64B31' },
|
||||
neutral: { name: '中性', items: [], color: '#FFAA16' },
|
||||
positive: { name: '正面', items: [], color: '#25C883' },
|
||||
};
|
||||
rowData.value.forEach((item) => {
|
||||
if (item.felling === 0) groups.negative.items.push(item);
|
||||
else if (item.felling === 1) groups.neutral.items.push(item);
|
||||
else if (item.felling === 2) groups.positive.items.push(item);
|
||||
});
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
const getKeywordTrendsList = async () => {
|
||||
const params = {
|
||||
industry_id: selectedIndustry.value,
|
||||
time_dimension: selectedTimePeriod.value,
|
||||
};
|
||||
const res = await fetchKeywordTrendsList(params);
|
||||
console.log('关键词热度榜', res);
|
||||
// 这里需要根据API返回的数据结构处理成tagRows需要的格式
|
||||
dataList.value = res;
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp) => {
|
||||
if (!timestamp) return '未记录';
|
||||
try {
|
||||
return dayjs.unix(timestamp).format('YYYY-MM-DD HH:mm');
|
||||
} catch (e) {
|
||||
console.error('时间格式转换错误', e);
|
||||
return '格式错误';
|
||||
}
|
||||
};
|
||||
|
||||
const getNewKeywordList = async () => {
|
||||
const params = {
|
||||
industry_id: selectedIndustry.value,
|
||||
time_dimension: selectedTimePeriod.value,
|
||||
};
|
||||
const res = await fetchNewKeywordList(params);
|
||||
// 这里需要根据API返回的数据结构处理成tagRows需要的格式
|
||||
keywordList.value = res;
|
||||
};
|
||||
|
||||
const drawChart = () => {
|
||||
var dom = document.getElementById('container');
|
||||
var myChart = echarts.init(dom, null, {
|
||||
renderer: 'canvas',
|
||||
useDirtyRect: false,
|
||||
});
|
||||
var option;
|
||||
option = {
|
||||
color: ['#25C883', '#F64B31'],
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
avoidLabelOverlap: false,
|
||||
data: fellingRate.value,
|
||||
labelLine: {
|
||||
show: false, // 不显示引导线
|
||||
},
|
||||
radius: ['40%', '55%'],
|
||||
},
|
||||
],
|
||||
};
|
||||
if (option && typeof option === 'object') {
|
||||
myChart.setOption(option);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听筛选条件变化
|
||||
watch([selectedIndustry, selectedTimePeriod], () => {
|
||||
getKeywordTrendsList();
|
||||
getIndustryEmotions();
|
||||
getNewKeywordList();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getKeywordTrendsList();
|
||||
getIndustryEmotions();
|
||||
getNewKeywordList();
|
||||
});
|
||||
|
||||
// const chartData = computed(() => {
|
||||
// const result = [
|
||||
// { name: '正面', value: 0, color: '#25C883' },
|
||||
// { name: '中性', value: 0, color: '#FFAA16' },
|
||||
// { name: '负面', value: 0, color: '#F64B31' },
|
||||
// ];
|
||||
|
||||
// rawData.value.forEach((item) => {
|
||||
// if (item.felling === 2) result[0].value++;
|
||||
// else if (item.felling === 1) result[1].value++;
|
||||
// else result[2].value++;
|
||||
// });
|
||||
|
||||
// return result.filter((item) => item.value > 0); // 过滤空数据
|
||||
// });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义样式 */
|
||||
:deep(.arco-table-th) {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
|
||||
:deep(.arco-table-tr):hover {
|
||||
background-color: var(--color-fill-1);
|
||||
}
|
||||
:deep(.arco-statistic-content .arco-statistic-value-integer) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pop-btn {
|
||||
background: #fff !important;
|
||||
border-color: #fff !important;
|
||||
color: #737478 !important;
|
||||
margin-left: -5px;
|
||||
}
|
||||
</style>
|
||||
217
src/views/components/dataEngine/topHeader.vue
Normal file
217
src/views/components/dataEngine/topHeader.vue
Normal file
@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 头部 -->
|
||||
<a-space
|
||||
direction="vertical"
|
||||
style="background-color: #fff; width: 100%; padding: 24px; margin: 24px 0; color: #737478; font-size: 14px"
|
||||
>
|
||||
<a-space align="center">
|
||||
<!-- 行业选择 -->
|
||||
<a-space align="center">
|
||||
<span>行业大类</span>
|
||||
<a-tag
|
||||
size="Medium"
|
||||
v-for="item in industriesTree"
|
||||
:key="item.id"
|
||||
:checkable="true"
|
||||
:checked="selectedIndustry == item.id"
|
||||
@check="handleIndustryCheck(item.id)"
|
||||
style="padding: 4px 16px; border-radius: 30px; height: 28px"
|
||||
:style="
|
||||
selectedIndustry == item.id
|
||||
? 'color: #6d4cfe; background-color: #f0edff'
|
||||
: 'color: #3C4043; background-color: #F7F8FA'
|
||||
"
|
||||
>{{ item.name }}</a-tag
|
||||
>
|
||||
</a-space>
|
||||
</a-space>
|
||||
<a-space align="center" style="margin-left: 'auto'; margin-top: 20px">
|
||||
<!-- 二级类目 -->
|
||||
<a-space align="center">
|
||||
<span>二级类目</span>
|
||||
<a-tag
|
||||
size="Medium"
|
||||
v-for="item in subCategories"
|
||||
:key="item.value"
|
||||
:checkable="true"
|
||||
:checked="selectedSubCategory == item.value"
|
||||
@check="handleSubCategoryCheck(item.value)"
|
||||
style="padding: 4px 16px; border-radius: 30px; height: 28px"
|
||||
:style="
|
||||
selectedSubCategory == item.value
|
||||
? 'color: #6d4cfe; background-color: #f0edff'
|
||||
: 'color: #3C4043; background-color: #F7F8FA'
|
||||
"
|
||||
>{{ item.label }}</a-tag
|
||||
>
|
||||
</a-space>
|
||||
</a-space>
|
||||
<a-space align="center" style="margin-left: 'auto'; margin-top: 20px">
|
||||
<!-- 时间筛选 -->
|
||||
<a-space align="center">
|
||||
<span>时间筛选</span>
|
||||
<a-tag
|
||||
size="Medium"
|
||||
v-for="item in timePeriods"
|
||||
:key="item.value"
|
||||
:checkable="true"
|
||||
:checked="selectedTimePeriod == item.value"
|
||||
@check="handleTimePeriodCheck(item.value)"
|
||||
style="padding: 4px 16px; border-radius: 30px; height: 28px"
|
||||
:style="
|
||||
selectedTimePeriod == item.value
|
||||
? 'color: #6d4cfe; background-color: #f0edff'
|
||||
: 'color: #3C4043; background-color: #F7F8FA'
|
||||
"
|
||||
>{{ item.label }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</a-space>
|
||||
<!-- 搜索区域 -->
|
||||
<a-space style="margin-left: 'auto'; margin-top: 20px">
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<icon-search />
|
||||
</template>
|
||||
<!-- Use the default slot to avoid extra spaces -->
|
||||
<template #default>搜索</template>
|
||||
</a-button>
|
||||
<a-button type="primary" style="background-color: #fff; color: #000">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
<!-- Use the default slot to avoid extra spaces -->
|
||||
<template #default>重置</template>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { fetchIndustriesTree, fetchIndustryTopics, fetchIndustryTopicDetail } from '@/api/all/index';
|
||||
import star1 from '@/assets/img/hottranslation/star-fill1.png';
|
||||
import star2 from '@/assets/img/hottranslation/star-fill2.png';
|
||||
import star3 from '@/assets/img/hottranslation/star-fill3.png';
|
||||
import star4 from '@/assets/img/hottranslation/star-fill4.png';
|
||||
import star5 from '@/assets/img/hottranslation/star-fill5.png';
|
||||
const starImages = [star1, star2, star3, star4, star5];
|
||||
// 行业大类
|
||||
const industriesTree = ref([]);
|
||||
// 数据状态
|
||||
const selectedIndustry = ref();
|
||||
const selectedSubCategory = ref('all');
|
||||
const selectedTimePeriod = ref('7');
|
||||
|
||||
// 暴露这些状态给父组件
|
||||
defineExpose({
|
||||
selectedIndustry,
|
||||
selectedSubCategory,
|
||||
selectedTimePeriod,
|
||||
});
|
||||
// 行业热门话题洞察
|
||||
const dataList = ref([]);
|
||||
// 显示详情
|
||||
const visible = ref(false);
|
||||
const topicInfo = ref({});
|
||||
// 二级类目选项
|
||||
const subCategories = [
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'airline', label: '航司' },
|
||||
{ value: 'hotel', label: '酒店' },
|
||||
{ value: 'entertainment', label: '玩乐' },
|
||||
{ value: 'cruise', label: '游轮' },
|
||||
];
|
||||
|
||||
// 时间周期选项
|
||||
const timePeriods = [
|
||||
{
|
||||
value: '7',
|
||||
label: '近7天',
|
||||
},
|
||||
{
|
||||
value: '15',
|
||||
label: '近15天',
|
||||
},
|
||||
{
|
||||
value: '30',
|
||||
label: '近30天',
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
getIndustriesTree();
|
||||
});
|
||||
// 获取行业大类数据
|
||||
const getIndustriesTree = async () => {
|
||||
const res = await fetchIndustriesTree();
|
||||
industriesTree.value = res;
|
||||
selectedIndustry.value = res[0].id;
|
||||
getIndustryTopics();
|
||||
};
|
||||
|
||||
// 行业热门话题
|
||||
const getIndustryTopics = async () => {
|
||||
let parms = {
|
||||
industry_id: selectedIndustry.value,
|
||||
time_dimension: selectedTimePeriod.value,
|
||||
};
|
||||
const res = await fetchIndustryTopics(parms);
|
||||
dataList.value = res;
|
||||
};
|
||||
const handleIndustryCheck = (value) => {
|
||||
selectedIndustry.value = value;
|
||||
};
|
||||
|
||||
const handleSubCategoryCheck = (value) => {
|
||||
selectedSubCategory.value = value;
|
||||
};
|
||||
|
||||
const handleTimePeriodCheck = (value) => {
|
||||
selectedTimePeriod.value = value;
|
||||
};
|
||||
|
||||
// 详情
|
||||
const gotoDetail = async (record) => {
|
||||
console.log(record);
|
||||
const res = await fetchIndustryTopicDetail(record.id);
|
||||
console.log(res);
|
||||
visible.value = true;
|
||||
topicInfo.value = res;
|
||||
};
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
getIndustryTopics();
|
||||
};
|
||||
|
||||
// 弹窗的取消
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 弹窗的确定
|
||||
const handleBeforeOk = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义样式 */
|
||||
:deep(.arco-table-th) {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
|
||||
:deep(.arco-table-tr):hover {
|
||||
background-color: var(--color-fill-1);
|
||||
}
|
||||
:deep(.arco-btn-outline) {
|
||||
color: #6d4cfe !important;
|
||||
border-color: #6d4cfe !important;
|
||||
}
|
||||
:deep(.arco-modal-body) {
|
||||
padding: 0px;
|
||||
}
|
||||
</style>
|
||||
191
src/views/components/dataEngine/userPainPoints.vue
Normal file
191
src/views/components/dataEngine/userPainPoints.vue
Normal file
@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<view>
|
||||
<topHeader ref="topHeaderRef"></topHeader>
|
||||
|
||||
<!-- 用户痛点观察 -->
|
||||
<a-space direction="vertical" style="background-color: #fff; width: 100%; padding: 24px; margin: 24px 0">
|
||||
<a-space align="center">
|
||||
<span>用户痛点观察 </span>
|
||||
<a-popover position="tl">
|
||||
<a-button type="primary" class="pop-btn">
|
||||
<template #icon>
|
||||
<icon-question-circle />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<p>基于xxx获取数据xxx,一段文字描述该数据的获取方式和来源等xxx</p>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
<a-table :data="dataList" :pagination="false">
|
||||
<template #columns>
|
||||
<a-table-column title="排名" data-index="rank">
|
||||
<template #cell="{ record }">
|
||||
<img v-if="record.rank == 1" :src="topImages[0]" style="width: 25px; height: 17px" />
|
||||
<img v-else-if="record.rank == 2" :src="topImages[1]" style="width: 25px; height: 17px" />
|
||||
<img v-else-if="record.rank == 3" :src="topImages[2]" style="width: 25px; height: 17px" />
|
||||
<span v-else>{{ record.rank }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="痛点名称" data-index="name" />
|
||||
<a-table-column title="关键词" data-index="keywords">
|
||||
<template #cell="{ record }">
|
||||
<a-tag v-for="item in record.keywords" :key="item" style="margin-right: 5px">{{ item }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="频次" data-index="frequency">
|
||||
<template #cell="{ record }">
|
||||
<a-tag v-if="record.frequency == 0" style="margin-right: 5px; background-color: #ebf7f2; color: #1bae71"
|
||||
>低频</a-tag
|
||||
>
|
||||
<a-tag
|
||||
v-else-if="record.frequency == 1"
|
||||
style="margin-right: 5px; background-color: #fff5de; color: #cc8b00"
|
||||
>中频</a-tag
|
||||
>
|
||||
<a-tag
|
||||
v-else-if="record.frequency == 2"
|
||||
style="margin-right: 5px; background-color: #ffe7e4; color: #c53c27"
|
||||
>高频</a-tag
|
||||
>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="代表性发言" data-index="content"> </a-table-column>
|
||||
<a-table-column title="操作" data-index="optional">
|
||||
<template #cell="{ record }">
|
||||
<a-button type="outline" @click="gotoDetail(record)">详情</a-button>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-space>
|
||||
|
||||
<a-modal :visible="visible" @ok="handleOk" @cancel="handleCancel" unmountOnClose>
|
||||
<template #title>
|
||||
<span style="text-align: left; width: 100%">用户痛点观察</span>
|
||||
</template>
|
||||
<div>
|
||||
<a-space direction="vertical" style="font-size: 12px">
|
||||
<a-space>
|
||||
<span style="margin-right: 16px; width: 60px">痛点</span>
|
||||
<span>{{ topicInfo.name }}</span>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<span style="margin-right: 16px; width: 60px; font-size: 12px">关键词</span>
|
||||
<a-tag v-for="item in topicInfo.keywords" :key="item" style="margin-right: 5px">{{ item }}</a-tag>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<span style="margin-right: 16px; width: 60px; font-size: 12px">频次</span>
|
||||
<a-tag v-if="topicInfo.frequency == 0" style="margin-right: 5px; background-color: #ebf7f2; color: #1bae71"
|
||||
>低频</a-tag
|
||||
>
|
||||
<a-tag
|
||||
v-else-if="topicInfo.frequency == 1"
|
||||
style="margin-right: 5px; background-color: #fff5de; color: #cc8b00"
|
||||
>中频</a-tag
|
||||
>
|
||||
<a-tag
|
||||
v-else-if="topicInfo.frequency == 2"
|
||||
style="margin-right: 5px; background-color: #ffe7e4; color: #c53c27"
|
||||
>高频</a-tag
|
||||
>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<span style="margin-right: 16px; width: 60px; font-size: 12px">代表性发言</span>
|
||||
<span>{{ topicInfo.content }}</span>
|
||||
</a-space>
|
||||
<a-space direction="top">
|
||||
<span style="margin-right: 16px; width: 60px; font-size: 12px">原始来源 </span>
|
||||
<a-space direction="vertical" style="margin-left: 15px">
|
||||
<a-space v-for="item in topicInfo.user_pain_point_sources" :key="item">
|
||||
<a-link style="background-color: initial" :href="item.link">{{ item.title }}</a-link>
|
||||
<img src="@/assets/img/hottranslation/xhs.png" style="width: 16px; height: 16px" />
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-modal>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import topHeader from './topHeader.vue';
|
||||
import { fetchUserPainPointsDetail, fetchUserPainPointsList } from '@/api/all/index';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import top1 from '@/assets/img/captcha/top1.svg';
|
||||
import top2 from '@/assets/img/captcha/top2.svg';
|
||||
import top3 from '@/assets/img/captcha/top3.svg';
|
||||
const topImages = [top1, top2, top3];
|
||||
const visible = ref(false);
|
||||
const topicInfo = ref({});
|
||||
const topHeaderRef = ref();
|
||||
// 从topHeader获取统一的状态
|
||||
const selectedIndustry = computed(() => topHeaderRef.value?.selectedIndustry);
|
||||
const selectedSubCategory = computed(() => topHeaderRef.value?.selectedSubCategory);
|
||||
const selectedTimePeriod = computed(() => topHeaderRef.value?.selectedTimePeriod);
|
||||
const dataList = ref([]);
|
||||
|
||||
// 详情
|
||||
const gotoDetail = async (record) => {
|
||||
console.log(record);
|
||||
const res = await fetchUserPainPointsDetail(record.id);
|
||||
console.log(res);
|
||||
visible.value = true;
|
||||
topicInfo.value = res;
|
||||
};
|
||||
|
||||
const getUserPainPointsList = async () => {
|
||||
const params = {
|
||||
industry_id: selectedIndustry.value,
|
||||
time_dimension: selectedTimePeriod.value,
|
||||
};
|
||||
const res = await fetchUserPainPointsList(params);
|
||||
console.log('关键词热度榜', res);
|
||||
// 这里需要根据API返回的数据结构处理成tagRows需要的格式
|
||||
dataList.value = res;
|
||||
};
|
||||
// 弹窗的取消
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 弹窗的确定
|
||||
const handleOk = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
// 监听筛选条件变化
|
||||
watch([selectedIndustry, selectedTimePeriod], () => {
|
||||
getUserPainPointsList();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getUserPainPointsList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义样式 */
|
||||
:deep(.arco-table-th) {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
|
||||
:deep(.arco-table-tr):hover {
|
||||
background-color: var(--color-fill-1);
|
||||
}
|
||||
:deep(.arco-statistic-content .arco-statistic-value-integer) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pop-btn {
|
||||
background: #fff !important;
|
||||
border-color: #fff !important;
|
||||
color: #737478 !important;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
:deep(.arco-btn-outline) {
|
||||
color: #6d4cfe !important;
|
||||
border-color: #6d4cfe !important;
|
||||
}
|
||||
</style>
|
||||
491
src/views/components/dataEngine/userPersona.vue
Normal file
491
src/views/components/dataEngine/userPersona.vue
Normal file
@ -0,0 +1,491 @@
|
||||
<template>
|
||||
<view>
|
||||
<topHeader ref="topHeaderRef"></topHeader>
|
||||
<a-space style="width: 100%; display: flex">
|
||||
<a-space direction="vertical" style="background-color: #fff; padding: 24px; flex: 1">
|
||||
<a-space align="center">
|
||||
<span>性别分布</span>
|
||||
<a-popover position="tl">
|
||||
<a-button type="primary" class="pop-btn">
|
||||
<template #icon>
|
||||
<icon-question-circle />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<p>基于xxx获取数据xxx,一段文字描述该数据的获取方式和来源等xxx</p>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<div id="container" style="height: 180px; width: 180px"></div>
|
||||
|
||||
<a-space direction="vertical" style="font-size: 14px">
|
||||
<a-space>
|
||||
<span style="width: 8px; height: 8px; background-color: #f64b31; border-radius: 50%"></span>
|
||||
<span>女性</span>
|
||||
<span style="width: 40px" v-if="genderData.length > 0">{{ genderData[0].rate * 100 }}%</span>
|
||||
<span>TGI</span>
|
||||
<span v-if="genderData.length > 0">{{ genderData[0].tgi }}</span>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<span style="width: 8px; height: 8px; background-color: #2a59f3; border-radius: 50%"></span>
|
||||
<span>男性</span>
|
||||
<span style="width: 40px" v-if="genderData.length > 1">{{ genderData[1].rate * 100 }}%</span>
|
||||
<span>TGI</span>
|
||||
<span v-if="genderData.length > 1">{{ genderData[1].tgi }}</span>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-space>
|
||||
<a-space direction="vertical" style="background-color: #fff; padding: 24px; flex: 1">
|
||||
<a-space style="display: flex; justify-content: space-between; width: 100%; font-size: 12px">
|
||||
<a-space align="center">
|
||||
<span>年龄分布</span>
|
||||
<a-popover position="tl">
|
||||
<a-button type="primary" class="pop-btn">
|
||||
<template #icon>
|
||||
<icon-question-circle />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<p>基于xxx获取数据xxx,一段文字描述该数据的获取方式和来源等xxx</p>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
<a-space align="center">
|
||||
<span style="width: 16px; height: 8px; background-color: #6d4cfe; border-radius: 2px"></span>
|
||||
<span style="color: #6d4cfe">占比</span>
|
||||
<span style="width: 16px; height: 8px; background-color: #f64b31; border-radius: 2px"></span>
|
||||
<span style="color: #f64b31">TGI比</span>
|
||||
</a-space>
|
||||
</a-space>
|
||||
|
||||
<a-space>
|
||||
<div id="age-container" style="height: 180px; width: 480px"></div>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-space>
|
||||
<a-space direction="vertical" style="background-color: #fff; padding: 24px; flex: 1; margin-top: 24px">
|
||||
<a-space align="center">
|
||||
<span>地域分布</span>
|
||||
<a-popover position="tl">
|
||||
<a-button type="primary" class="pop-btn">
|
||||
<template #icon>
|
||||
<icon-question-circle />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<p>基于xxx获取数据xxx,一段文字描述该数据的获取方式和来源等xxx</p>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<a-space direction="vertical">
|
||||
<div id="chinaMap" style="height: 416px; width: 640px"></div>
|
||||
<a-space direction="vertical" style="font-size: 14px">
|
||||
<span>搜索指数</span>
|
||||
<a-space>
|
||||
<span>高</span>
|
||||
<span
|
||||
v-for="item in 5"
|
||||
:key="item"
|
||||
:style="{
|
||||
width: `${18 - item * 2}px`,
|
||||
height: `${18 - item * 2}px`,
|
||||
backgroundColor: '#6D4CFE',
|
||||
borderRadius: '50%',
|
||||
display: 'inline-block',
|
||||
margin: '0 2px',
|
||||
}"
|
||||
></span>
|
||||
<span>低</span>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<a-tabs default-active-key="1" @change="tabChange">
|
||||
<a-tab-pane key="1" title="省份">
|
||||
<a-table :data="geoList" :pagination="false">
|
||||
<template #columns>
|
||||
<a-table-column title="排名" data-index="rank" />
|
||||
<a-table-column title="省份" data-index="geo" />
|
||||
<a-table-column title="分布占比" data-index="rate" />
|
||||
|
||||
<a-table-column title="TGI指数" data-index="tgi" />
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" title="城市">
|
||||
<a-table :data="geoList" :pagination="false">
|
||||
<template #columns>
|
||||
<a-table-column title="排名" data-index="rank" />
|
||||
<a-table-column title="城市" data-index="geo" />
|
||||
<a-table-column title="分布占比" data-index="rate" />
|
||||
|
||||
<a-table-column title="TGI指数" data-index="tgi" />
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import topHeader from './topHeader.vue';
|
||||
import { fetchAgeDistributionsList, fetchGeoDistributionsList, fetchGenderDistributionsList } from '@/api/all/index';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import chinaJson from '@/assets/maps/china.json';
|
||||
|
||||
echarts.registerMap('china', chinaJson);
|
||||
const scope = ref(1); //地域范围,1-省,2-市
|
||||
const chartInstance = (ref < echarts.ECharts) | (null > null);
|
||||
const topHeaderRef = ref();
|
||||
// 从topHeader获取统一的状态
|
||||
const selectedIndustry = computed(() => topHeaderRef.value?.selectedIndustry);
|
||||
const selectedSubCategory = computed(() => topHeaderRef.value?.selectedSubCategory);
|
||||
const selectedTimePeriod = computed(() => topHeaderRef.value?.selectedTimePeriod);
|
||||
const genderData = ref([]);
|
||||
const genderValueData = ref([]);
|
||||
const ageValueData = ref([]);
|
||||
const geoList = ref([]);
|
||||
// 监听筛选条件变化
|
||||
watch([selectedIndustry, selectedTimePeriod], () => {
|
||||
getAgeDistributionsList();
|
||||
getGeoDistributionsList();
|
||||
getGenderDistributionsList();
|
||||
drawChinaMap();
|
||||
});
|
||||
|
||||
// 获取年龄分布列表
|
||||
const getAgeDistributionsList = async () => {
|
||||
const params = {
|
||||
industry_id: selectedIndustry.value,
|
||||
time_dimension: selectedTimePeriod.value,
|
||||
};
|
||||
const res = await fetchAgeDistributionsList(params);
|
||||
console.log('年龄分布:', res);
|
||||
ageValueData.value = res;
|
||||
drawAgeChart();
|
||||
};
|
||||
|
||||
// 获得地理分布列表
|
||||
const getGeoDistributionsList = async () => {
|
||||
const params = {
|
||||
scope: scope.value,
|
||||
industry_id: selectedIndustry.value,
|
||||
time_dimension: selectedTimePeriod.value,
|
||||
};
|
||||
const res = await fetchGeoDistributionsList(params);
|
||||
console.log('地理分布:', res);
|
||||
geoList.value = res;
|
||||
};
|
||||
// 获取性别分布列表
|
||||
const getGenderDistributionsList = async () => {
|
||||
const params = {
|
||||
industry_id: selectedIndustry.value,
|
||||
time_dimension: selectedTimePeriod.value,
|
||||
};
|
||||
const res = await fetchGenderDistributionsList(params);
|
||||
genderData.value = [];
|
||||
genderData.value = [...res];
|
||||
await nextTick();
|
||||
genderValueData.value = [];
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
genderValueData.value.push({
|
||||
value: res[i].rate * 100,
|
||||
});
|
||||
}
|
||||
|
||||
drawChart();
|
||||
};
|
||||
|
||||
const drawChart = () => {
|
||||
var dom = document.getElementById('container');
|
||||
var myChart = echarts.init(dom, null, {
|
||||
renderer: 'canvas',
|
||||
useDirtyRect: false,
|
||||
});
|
||||
var option;
|
||||
option = {
|
||||
color: ['#F64B31', '#2A59F3'],
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
avoidLabelOverlap: false,
|
||||
data: genderValueData.value,
|
||||
labelLine: {
|
||||
show: false, // 不显示引导线
|
||||
},
|
||||
radius: ['40%', '55%'],
|
||||
},
|
||||
],
|
||||
};
|
||||
if (option && typeof option === 'object') {
|
||||
myChart.setOption(option);
|
||||
}
|
||||
};
|
||||
|
||||
const drawAgeChart = () => {
|
||||
const dom = document.getElementById('age-container');
|
||||
// 数据准备
|
||||
const xAxis = ageValueData.value.map((item) => item.age);
|
||||
const yAxis = ageValueData.value.map((item) => item.rate * 100);
|
||||
const yAxis2 = ageValueData.value.map((item) => item.tgi);
|
||||
const average = yAxis2.reduce((sum, val) => sum + val, 0) / yAxis2.length;
|
||||
|
||||
// 图表初始化(强制使用2D渲染)
|
||||
const myChart = echarts.init(dom, null, {
|
||||
renderer: 'canvas',
|
||||
useDirtyRect: false,
|
||||
devicePixelRatio: window.devicePixelRatio * 1.5, // 提高渲染精度
|
||||
});
|
||||
|
||||
// 动态计算刻度间隔
|
||||
const calcInterval = (data, base = 10) => {
|
||||
const range = Math.max(...data) - Math.min(...data);
|
||||
return Math.max(base, Math.ceil(range / 4 / base) * base);
|
||||
};
|
||||
|
||||
// 核心配置
|
||||
const option = {
|
||||
animation: false, // 小尺寸下关闭动画提升性能
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
confine: true, // 确保tooltip不超出容器
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
fontSize: 12, // 调小提示标签字号
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
top: 10,
|
||||
right: 30,
|
||||
bottom: 25,
|
||||
left: 40,
|
||||
containLabel: false,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxis,
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 0,
|
||||
fontSize: 12,
|
||||
margin: 2,
|
||||
hideOverlap: true, // 自动隐藏重叠标签
|
||||
},
|
||||
axisTick: {
|
||||
alignWithLabel: true,
|
||||
length: 3, // 缩短刻度线
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 0.5, // 减细轴线
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
// 左侧百分比轴
|
||||
type: 'value',
|
||||
name: '占比',
|
||||
nameLocation: 'top',
|
||||
nameGap: 8,
|
||||
nameTextStyle: {
|
||||
fontSize: 12,
|
||||
padding: [0, 0, 3, 0], // 微调名称位置
|
||||
},
|
||||
min: 0,
|
||||
max: Math.ceil(Math.max(...yAxis) / 20) * 20,
|
||||
interval: calcInterval(yAxis, 20),
|
||||
axisLabel: {
|
||||
formatter: '{value}%',
|
||||
fontSize: 8,
|
||||
margin: 4,
|
||||
showMinLabel: true,
|
||||
showMaxLabel: true,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: 'rgba(255,255,255,0.3)',
|
||||
width: 0.8,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// 右侧TGI轴
|
||||
type: 'value',
|
||||
name: 'TGI指数',
|
||||
nameLocation: 'top',
|
||||
nameGap: 8,
|
||||
nameTextStyle: {
|
||||
fontSize: 10,
|
||||
color: '#F64B31',
|
||||
padding: [0, 0, 3, 0],
|
||||
},
|
||||
min: Math.floor(Math.min(...yAxis2) / 50) * 50,
|
||||
max: Math.ceil(Math.max(...yAxis2) / 50) * 50,
|
||||
interval: calcInterval(yAxis2, 50),
|
||||
axisLabel: {
|
||||
fontSize: 8,
|
||||
margin: 4,
|
||||
color: '#F64B31',
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#F64B31',
|
||||
width: 0.8,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
// 柱状图
|
||||
name: '占比',
|
||||
type: 'bar',
|
||||
barWidth: 10,
|
||||
itemStyle: {
|
||||
color: '#6d4cfe',
|
||||
borderRadius: [1, 1, 0, 0], // 微圆角
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
formatter: '{c}%',
|
||||
fontSize: 8,
|
||||
color: '#fff',
|
||||
distance: 1, // 减小标签与柱子的距离
|
||||
},
|
||||
data: yAxis,
|
||||
},
|
||||
{
|
||||
// 折线图
|
||||
name: 'TGI',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#F64B31',
|
||||
width: 1.8,
|
||||
},
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#FFAE00',
|
||||
width: 1.2,
|
||||
type: 'dashed',
|
||||
},
|
||||
label: {
|
||||
fontSize: 8,
|
||||
color: '#FFAE00',
|
||||
formatter: (params) => {
|
||||
// 改用回调函数
|
||||
const avg = params.data.coord[1]; // 获取平均值坐标
|
||||
return 'TGI:' + avg.toFixed(0); // 格式化显示
|
||||
},
|
||||
position: 'end',
|
||||
},
|
||||
data: [{ type: 'average' }],
|
||||
},
|
||||
data: yAxis2,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 应用配置
|
||||
myChart.setOption(option);
|
||||
|
||||
// 超小尺寸适配
|
||||
const handleResize = () => {
|
||||
const currentWidth = dom.clientWidth;
|
||||
if (currentWidth < 400) {
|
||||
myChart.setOption({
|
||||
xAxis: { axisLabel: { rotate: 90, fontSize: 7 } },
|
||||
grid: { bottom: 35 },
|
||||
});
|
||||
}
|
||||
myChart.resize();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
|
||||
const tabChange = (val) => {
|
||||
scope.value = val;
|
||||
getGeoDistributionsList();
|
||||
};
|
||||
|
||||
const drawChinaMap = () => {
|
||||
const dom = document.getElementById('chinaMap');
|
||||
// 图表初始化(强制使用2D渲染)
|
||||
const myChart = echarts.init(dom, null, {
|
||||
renderer: 'canvas',
|
||||
useDirtyRect: false,
|
||||
devicePixelRatio: window.devicePixelRatio * 1.5, // 提高渲染精度
|
||||
});
|
||||
|
||||
const option = {
|
||||
series: [
|
||||
{
|
||||
name: '中国',
|
||||
type: 'map',
|
||||
map: 'china',
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
data: [
|
||||
{ name: '北京', value: 1000 },
|
||||
{ name: '上海', value: 800 },
|
||||
{ name: '广东', value: 900 },
|
||||
// 其他省份数据...
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
myChart.setOption(option);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getAgeDistributionsList();
|
||||
getGeoDistributionsList();
|
||||
getGenderDistributionsList();
|
||||
drawChinaMap();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义样式 */
|
||||
:deep(.arco-table-th) {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
|
||||
:deep(.arco-table-tr):hover {
|
||||
background-color: var(--color-fill-1);
|
||||
}
|
||||
:deep(.arco-statistic-content .arco-statistic-value-integer) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pop-btn {
|
||||
background: #fff !important;
|
||||
border-color: #fff !important;
|
||||
color: #737478 !important;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
:deep(.arco-btn-outline) {
|
||||
color: #6d4cfe !important;
|
||||
border-color: #6d4cfe !important;
|
||||
}
|
||||
</style>
|
||||
428
src/views/components/drag/DraggableResizable.vue
Normal file
428
src/views/components/drag/DraggableResizable.vue
Normal file
@ -0,0 +1,428 @@
|
||||
<template>
|
||||
<div :id="id" ref="container" class="draggable-resizable" :style="containerStyle">
|
||||
<div class="header" @mousedown.prevent="onMouseDown" :style="{ cursor: mouseCursor }">
|
||||
{{ id }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="dir in directions"
|
||||
:key="dir"
|
||||
:class="['resize-handle', dir]"
|
||||
@mousedown.stop.prevent="onResizeHandleMouseDown(dir, $event)"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="DraggableResizable">
|
||||
import type { PropType } from 'vue';
|
||||
import { ref, reactive, defineProps, defineEmits } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
x: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
y: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
zIndex: {
|
||||
type: String,
|
||||
default: '1',
|
||||
required: true,
|
||||
},
|
||||
snapDistance: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
onDrag: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
onResize: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
detectCollision: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
detectSnap: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
checkClosestComponent: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
setCurrentComponent: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
handleCollision: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
directions: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: false,
|
||||
default: ['top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['drag', 'resize']);
|
||||
const mouseCursor = ref('grab');
|
||||
|
||||
const isCollied = ref(false);
|
||||
const isSnap = ref(false);
|
||||
const startX = ref(0);
|
||||
const startY = ref(0);
|
||||
const startLeft = ref(0);
|
||||
const startTop = ref(0);
|
||||
|
||||
const container = ref<HTMLElement | null>(null);
|
||||
const ghost = ref<HTMLElement | null>(null);
|
||||
|
||||
const containerStyle = reactive({
|
||||
id: `${props.id}`,
|
||||
width: `${props.width}px`,
|
||||
height: `${props.height}px`,
|
||||
top: `${props.y}px`,
|
||||
left: `${props.x}px`,
|
||||
zIndex: props.zIndex,
|
||||
transition: 'none',
|
||||
});
|
||||
|
||||
const updatePosition = (left: number, top: number, snap = false) => {
|
||||
isSnap.value = snap;
|
||||
if (isSnap.value) {
|
||||
containerStyle.transition = 'left 0.08s ease-out, top 0.08s ease-out';
|
||||
} else {
|
||||
containerStyle.transition = 'none';
|
||||
}
|
||||
containerStyle.left = `${left}px`;
|
||||
containerStyle.top = `${top}px`;
|
||||
};
|
||||
|
||||
const updateGhostPosition = (left: number, top: number) => {
|
||||
if (ghost.value) {
|
||||
ghost.value.style.left = `${left}px`;
|
||||
ghost.value.style.top = `${top}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const updateSize = (width: number, height: number) => {
|
||||
containerStyle.width = `${width}px`;
|
||||
containerStyle.height = `${height}px`;
|
||||
};
|
||||
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
mouseCursor.value = 'grabbing';
|
||||
startX.value = event.clientX;
|
||||
startY.value = event.clientY;
|
||||
startLeft.value = parseInt(containerStyle.left);
|
||||
startTop.value = parseInt(containerStyle.top);
|
||||
props.setCurrentComponent(containerStyle);
|
||||
containerStyle.zIndex = '3';
|
||||
|
||||
ghost.value = document.createElement('div');
|
||||
ghost.value.style.width = containerStyle.width;
|
||||
ghost.value.style.height = containerStyle.height;
|
||||
ghost.value.style.backgroundColor = '#E33B54';
|
||||
ghost.value.style.zIndex = '1';
|
||||
ghost.value.style.position = 'absolute';
|
||||
ghost.value.style.left = containerStyle.left;
|
||||
ghost.value.style.top = containerStyle.top;
|
||||
const fatherEle = document.getElementById(props.id);
|
||||
fatherEle?.appendChild(ghost.value);
|
||||
|
||||
const onMouseMove = (moveEvent: MouseEvent) => {
|
||||
//* 使用 requestAnimationFrame 来优化性能
|
||||
requestAnimationFrame(() => {
|
||||
const newLeft = startLeft.value + (moveEvent.clientX - startX.value);
|
||||
const newTop = startTop.value + (moveEvent.clientY - startY.value);
|
||||
updateGhostPosition(newLeft, newTop);
|
||||
// const closestComponent = props.checkClosestComponent(
|
||||
// props.id,
|
||||
// newLeft,
|
||||
// newTop,
|
||||
// parseInt(containerStyle.width),
|
||||
// parseInt(containerStyle.height),
|
||||
// );
|
||||
//* console.log('closestComponent', closestComponent);
|
||||
updatePosition(newLeft, newTop, false);
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseUp = (moveEvent: MouseEvent) => {
|
||||
moveAndResize(moveEvent);
|
||||
mouseCursor.value = 'grab';
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
if (isCollied.value) {
|
||||
containerStyle.zIndex = '3';
|
||||
} else {
|
||||
containerStyle.zIndex = '1';
|
||||
isCollied.value = false;
|
||||
}
|
||||
containerStyle.transition = 'none';
|
||||
console.log('containerStyle', containerStyle);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
ghost.value.parentNode?.removeChild(ghost.value);
|
||||
};
|
||||
|
||||
const moveAndResize = (moveEvent: MouseEvent) => {
|
||||
const newLeft = startLeft.value + (moveEvent.clientX - startX.value);
|
||||
const newTop = startTop.value + (moveEvent.clientY - startY.value);
|
||||
console.log(newLeft);
|
||||
console.log(newTop);
|
||||
|
||||
// containerStyle.zIndex = '3';
|
||||
|
||||
//* 使用 requestAnimationFrame 来优化性能
|
||||
requestAnimationFrame(() => {
|
||||
//* 检测吸附
|
||||
const snapResult = props.detectSnap(
|
||||
props.id,
|
||||
newLeft,
|
||||
newTop,
|
||||
parseInt(containerStyle.width),
|
||||
parseInt(containerStyle.height),
|
||||
props.snapDistance,
|
||||
);
|
||||
let finalLeft = snapResult.left;
|
||||
let finalTop = snapResult.top;
|
||||
isSnap.value = snapResult.isSnap;
|
||||
console.log('finalLeft', finalLeft);
|
||||
console.log('finalTop', finalTop);
|
||||
|
||||
const colliedInfo = props.detectCollision(
|
||||
props.id,
|
||||
finalLeft,
|
||||
finalTop,
|
||||
parseInt(containerStyle.width),
|
||||
parseInt(containerStyle.height),
|
||||
);
|
||||
|
||||
isCollied.value = colliedInfo.value;
|
||||
|
||||
updatePosition(finalLeft, finalTop, isSnap.value);
|
||||
emit('drag', props.id, parseInt(containerStyle.left), parseInt(containerStyle.top));
|
||||
|
||||
//* 检测边界和碰撞
|
||||
//todo
|
||||
//* if (!colliedInfo.value) {
|
||||
//* updatePosition(finalLeft, finalTop, isSnap);
|
||||
//* } else {
|
||||
//* const colliedComponent = colliedInfo.colliedComponent;
|
||||
//* const collidedDirection = colliedInfo.collidedDirection;
|
||||
//* if (colliedComponent && collidedDirection) {
|
||||
//* //* 左右两边碰撞,则只移动垂直方向
|
||||
//* if (
|
||||
//* collidedDirection === 'left' ||
|
||||
//* (collidedDirection === 'right' &&
|
||||
//* (parseInt(containerStyle.height) + finalTop >= colliedComponent.y ||
|
||||
//* finalTop <= colliedComponent.y + parseInt(colliedComponent.height)))
|
||||
//* ) {
|
||||
//* containerStyle.top = `${finalTop}px`;
|
||||
//* }
|
||||
//* //* 上下两边碰撞,则只移动水平方向
|
||||
//* if (
|
||||
//* collidedDirection === 'top' ||
|
||||
//* (collidedDirection === 'bottom' &&
|
||||
//* (parseInt(containerStyle.width) + finalLeft >= colliedComponent.x ||
|
||||
//* finalLeft <= colliedComponent.x + parseInt(colliedComponent.width)))
|
||||
//* ) {
|
||||
//* containerStyle.left = `${finalLeft}px`;
|
||||
//* }
|
||||
//* }
|
||||
//* }
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 组件resize事件
|
||||
* @param dir
|
||||
* @param event
|
||||
*/
|
||||
const onResizeHandleMouseDown = (dir: string, event: MouseEvent) => {
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
const startWidth = parseInt(containerStyle.width);
|
||||
const startHeight = parseInt(containerStyle.height);
|
||||
const startLeft = parseInt(containerStyle.left);
|
||||
const startTop = parseInt(containerStyle.top);
|
||||
|
||||
const onMouseMove = (moveEvent: MouseEvent) => {
|
||||
let newWidth = startWidth;
|
||||
let newHeight = startHeight;
|
||||
let newLeft = startLeft;
|
||||
let newTop = startTop;
|
||||
|
||||
if (dir.includes('right')) {
|
||||
newWidth = startWidth + (moveEvent.clientX - startX);
|
||||
} else if (dir.includes('left')) {
|
||||
newWidth = startWidth - (moveEvent.clientX - startX);
|
||||
newLeft = startLeft + (moveEvent.clientX - startX);
|
||||
}
|
||||
|
||||
if (dir.includes('bottom')) {
|
||||
newHeight = startHeight + (moveEvent.clientY - startY);
|
||||
} else if (dir.includes('top')) {
|
||||
newHeight = startHeight - (moveEvent.clientY - startY);
|
||||
newTop = startTop + (moveEvent.clientY - startY);
|
||||
}
|
||||
|
||||
//* 使用 requestAnimationFrame 来优化性能
|
||||
requestAnimationFrame(() => {
|
||||
//* 检测吸附
|
||||
const snapResult = props.detectSnap(props.id, newLeft, newTop, newWidth, newHeight, props.snapDistance);
|
||||
let finalWidth = snapResult.width;
|
||||
let finalHeight = snapResult.height;
|
||||
let finalLeft = snapResult.left;
|
||||
let finalTop = snapResult.top;
|
||||
isSnap.value = snapResult.isSnap;
|
||||
|
||||
updateSize(finalWidth, finalHeight);
|
||||
updatePosition(finalLeft, finalTop, isSnap.value);
|
||||
|
||||
return;
|
||||
|
||||
const colliedInfo = props.detectCollision(props.id, finalLeft, finalTop, finalWidth, finalHeight);
|
||||
//* 检测碰撞
|
||||
if (!colliedInfo.value) {
|
||||
updateSize(finalWidth, finalHeight);
|
||||
updatePosition(finalLeft, finalTop, isSnap.value);
|
||||
} else {
|
||||
//todo 当元素的某一边已经吸附,且resize的方向并非和吸附的方向一致,则仍可以进行updateSize
|
||||
const colliedComponent = colliedInfo.colliedComponent;
|
||||
const collidedDirection = colliedInfo.collidedDirection;
|
||||
if (colliedComponent && collidedDirection) {
|
||||
//* 左右两边碰撞,则只移动垂直方向
|
||||
if (
|
||||
collidedDirection === 'left' ||
|
||||
(collidedDirection === 'right' &&
|
||||
(parseInt(containerStyle.height) + finalTop >= colliedComponent.y ||
|
||||
finalTop <= colliedComponent.y + parseInt(colliedComponent.height)))
|
||||
) {
|
||||
containerStyle.top = `${finalTop}px`;
|
||||
}
|
||||
//* 上下两边碰撞,则只移动水平方向
|
||||
if (
|
||||
collidedDirection === 'top' ||
|
||||
(collidedDirection === 'bottom' &&
|
||||
(parseInt(containerStyle.width) + finalLeft >= colliedComponent.x ||
|
||||
finalLeft <= colliedComponent.x + parseInt(colliedComponent.width)))
|
||||
) {
|
||||
containerStyle.left = `${finalLeft}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
emit('resize', props.id, parseInt(containerStyle.width), parseInt(containerStyle.height));
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
//* 使用 ResizeObserver 来监听容器大小变化
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
console.log(`entries, ${entries}`);
|
||||
const entry = entries[0];
|
||||
const { width, height } = entry.contentRect;
|
||||
updateSize(width, height);
|
||||
});
|
||||
|
||||
//* 在容器元素存在时,才监听其大小变化
|
||||
if (container.value) {
|
||||
resizeObserver.observe(container.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.draggable-resizable {
|
||||
position: absolute;
|
||||
background-color: lightblue;
|
||||
border: 1px solid #333;
|
||||
box-sizing: border-box;
|
||||
.header {
|
||||
width: 100%;
|
||||
background-color: #333;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid #333;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #333;
|
||||
z-index: 1;
|
||||
}
|
||||
.resize-handle.top {
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
cursor: n-resize;
|
||||
}
|
||||
.resize-handle.bottom {
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
cursor: s-resize;
|
||||
}
|
||||
.resize-handle.left {
|
||||
left: -5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: w-resize;
|
||||
}
|
||||
.resize-handle.right {
|
||||
right: -5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: e-resize;
|
||||
}
|
||||
.resize-handle.top-left {
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
cursor: nw-resize;
|
||||
}
|
||||
.resize-handle.top-right {
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
cursor: ne-resize;
|
||||
}
|
||||
.resize-handle.bottom-left {
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
cursor: sw-resize;
|
||||
}
|
||||
.resize-handle.bottom-right {
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
cursor: se-resize;
|
||||
}
|
||||
</style>
|
||||
459
src/views/components/drag/index.vue
Normal file
459
src/views/components/drag/index.vue
Normal file
@ -0,0 +1,459 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<DraggableResizable
|
||||
v-for="comp in components"
|
||||
:key="comp.id"
|
||||
:id="comp.id"
|
||||
:x="comp.x"
|
||||
:y="comp.y"
|
||||
:width="comp.width"
|
||||
:height="comp.height"
|
||||
:zIndex="comp.zIndex"
|
||||
:snapDistance="snapDistance"
|
||||
@drag="handleDrag"
|
||||
@resize="handleResize"
|
||||
:detectCollision="detectCollision"
|
||||
:detectSnap="detectSnap"
|
||||
:handleCollision="handleCollision"
|
||||
:checkClosestComponent="getClosestComponent"
|
||||
:setCurrentComponent="getCurrentComponent"
|
||||
:directions="['right', 'bottom-right']"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
//todo 1 增加布局切换的功能,且切换有过渡效果
|
||||
//todo 2 吸附功能也需要增加过渡效果
|
||||
//todo 3 resize的操作节点需要可配置,并且允许用svg替代
|
||||
//todo 4 拖拽区域需要可配置
|
||||
//todo 5 增加drag预期位置显示,并设置吸附距离,达到吸附距离后立即移动到吸附后的位置,并增加过渡效果
|
||||
//todo 6 当预期位置侵占其他组件时,需要移动被侵占组件的位置
|
||||
import DraggableResizable from './DraggableResizable.vue';
|
||||
interface ComponentState {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
zIndex?: number | string;
|
||||
}
|
||||
interface SnapResult {
|
||||
id?: string;
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
cx?: number;
|
||||
cy?: number;
|
||||
isSnap: boolean;
|
||||
}
|
||||
const snapDistance = 50;
|
||||
const defaultComponents = [
|
||||
{ id: 'comp1', x: 100, y: 100, width: 200, height: 200, zIndex: 1 },
|
||||
{ id: 'comp2', x: 400, y: 100, width: 200, height: 200, zIndex: 1 },
|
||||
{ id: 'comp3', x: 100, y: 400, width: 200, height: 200, zIndex: 1 },
|
||||
{ id: 'comp4', x: 400, y: 400, width: 200, height: 200, zIndex: 1 },
|
||||
];
|
||||
const components = reactive<ComponentState[]>([]);
|
||||
const currentComp = reactive<ComponentState>({
|
||||
id: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
const parentWidth = ref(0);
|
||||
const parentHeight = ref(0);
|
||||
|
||||
const loadState = () => {
|
||||
const savedState = localStorage.getItem('componentsState');
|
||||
if (savedState) {
|
||||
Object.assign(components, JSON.parse(savedState));
|
||||
} else {
|
||||
Object.assign(components, defaultComponents);
|
||||
}
|
||||
};
|
||||
|
||||
const saveState = () => {
|
||||
localStorage.setItem('componentsState', JSON.stringify(components));
|
||||
};
|
||||
|
||||
const handleDrag = (id: string, x: number, y: number) => {
|
||||
const component = components.find((c) => c.id === id);
|
||||
if (component) {
|
||||
component.x = x;
|
||||
component.y = y;
|
||||
saveState();
|
||||
}
|
||||
};
|
||||
|
||||
const handleResize = (id: string, width: number, height: number) => {
|
||||
const component = components.find((c) => c.id === id);
|
||||
if (component) {
|
||||
component.width = width;
|
||||
component.height = height;
|
||||
saveState();
|
||||
}
|
||||
};
|
||||
|
||||
type CollidedDirection = 'top' | 'right' | 'bottom' | 'left' | '';
|
||||
|
||||
/**
|
||||
* 检测元素之间的碰撞
|
||||
* @param id
|
||||
* @param x
|
||||
* @param y
|
||||
* @param width
|
||||
* @param height
|
||||
*/
|
||||
const detectCollision = (id: string, x: number, y: number, width: number, height: number) => {
|
||||
const currentComponent = components.find((c) => c.id === id);
|
||||
if (!currentComponent) return false;
|
||||
let colliedComponent: ComponentState | null = null;
|
||||
let collidedDirection: CollidedDirection = '';
|
||||
const value = components.some((c) => {
|
||||
if (c.id === id) return false;
|
||||
colliedComponent = c;
|
||||
return !(x + width <= c.x || x >= c.x + c.width || y + height <= c.y || y >= c.y + c.height);
|
||||
});
|
||||
if (value && colliedComponent) {
|
||||
collidedDirection = getColliedDirection<ComponentState>(currentComponent, colliedComponent);
|
||||
}
|
||||
return { value, colliedComponent, collidedDirection };
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取被碰撞组件的方向
|
||||
* @param currentComponent
|
||||
* @param colliedComponent
|
||||
*/
|
||||
const getColliedDirection = <T extends ComponentState>(currentComponent: T, colliedComponent: T): CollidedDirection => {
|
||||
let collidedDirection: CollidedDirection = '';
|
||||
if (currentComponent.x + currentComponent.width === colliedComponent.x) {
|
||||
collidedDirection = 'left';
|
||||
}
|
||||
if (currentComponent.x === colliedComponent.x + colliedComponent.width) {
|
||||
collidedDirection = 'right';
|
||||
}
|
||||
if (currentComponent.y + currentComponent.height === colliedComponent.y) {
|
||||
collidedDirection = 'top';
|
||||
}
|
||||
if (currentComponent.y === colliedComponent.y + colliedComponent.height) {
|
||||
collidedDirection = 'bottom';
|
||||
}
|
||||
return collidedDirection;
|
||||
};
|
||||
|
||||
const getCurrentComponent = (currentComponent: {
|
||||
width: string;
|
||||
height: string;
|
||||
top: string;
|
||||
left: string;
|
||||
id: string;
|
||||
}): ComponentState => {
|
||||
return Object.assign(currentComp, {
|
||||
id: currentComponent.id,
|
||||
x: parseInt(currentComponent.left),
|
||||
y: parseInt(currentComponent.top),
|
||||
width: parseInt(currentComponent.width),
|
||||
height: parseInt(currentComponent.height),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测吸附
|
||||
* @param id
|
||||
* @param x
|
||||
* @param y
|
||||
* @param width
|
||||
* @param height
|
||||
* @param snapDistance
|
||||
*/
|
||||
const detectSnap = (id: string, x: number, y: number, width: number, height: number, snapDistance: number) => {
|
||||
let snapResult: SnapResult = { left: x, top: y, width, height, cx: 0, cy: 0, isSnap: false };
|
||||
|
||||
const closestComponent = getClosestComponent(id, x, y, width, height);
|
||||
// console.log('closestComponent', closestComponent);
|
||||
|
||||
//* 检测左边缘吸附
|
||||
if (Math.abs(x - (closestComponent.x + closestComponent.width)) <= snapDistance) {
|
||||
snapResult.left = closestComponent.x + closestComponent.width;
|
||||
console.log('snap left', snapResult.left);
|
||||
snapResult.isSnap = true;
|
||||
}
|
||||
//* 检测右边缘吸附
|
||||
else if (Math.abs(x + width - closestComponent.x) <= snapDistance) {
|
||||
snapResult.left = closestComponent.x - width;
|
||||
console.log('snap right', snapResult.left);
|
||||
snapResult.isSnap = true;
|
||||
}
|
||||
//* 检测上边缘吸附
|
||||
else if (
|
||||
Math.abs(y - (closestComponent.y + closestComponent.height)) <= snapDistance &&
|
||||
(Math.abs(x + width - closestComponent.x) <= snapDistance || x + width >= closestComponent.x)
|
||||
) {
|
||||
snapResult.top = closestComponent.y + closestComponent.height;
|
||||
console.log('snap top', snapResult.top);
|
||||
snapResult.isSnap = true;
|
||||
}
|
||||
//* 检测下边缘吸附
|
||||
else if (Math.abs(y + height - closestComponent.y) <= snapDistance) {
|
||||
snapResult.top = closestComponent.y - height;
|
||||
console.log('snap bottom', snapResult.top);
|
||||
snapResult.isSnap = true;
|
||||
} else {
|
||||
snapResult.cx = closestComponent.x;
|
||||
snapResult.cy = closestComponent.y;
|
||||
}
|
||||
|
||||
//* 检测父元素边界吸附
|
||||
if (x <= snapDistance) {
|
||||
console.log('parent left');
|
||||
snapResult.left = 0;
|
||||
snapResult.isSnap = true;
|
||||
}
|
||||
if (y <= snapDistance) {
|
||||
console.log('parent top');
|
||||
snapResult.top = 0;
|
||||
snapResult.isSnap = true;
|
||||
}
|
||||
if (parentWidth.value - (x + width) <= snapDistance) {
|
||||
console.log('parent right');
|
||||
snapResult.left = parentWidth.value - width;
|
||||
snapResult.isSnap = true;
|
||||
}
|
||||
if (parentHeight.value - (y + height) <= snapDistance) {
|
||||
console.log('parent bottom');
|
||||
snapResult.top = parentHeight.value - height;
|
||||
snapResult.isSnap = true;
|
||||
}
|
||||
|
||||
// const result = adjustSnapResult(id, x, y, width, height, snapResult);
|
||||
// Object.assign(snapResult, result);
|
||||
|
||||
return snapResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* 调整吸附参数
|
||||
* @param id
|
||||
* @param x
|
||||
* @param y
|
||||
* @param width
|
||||
* @param height
|
||||
* @param snapResult
|
||||
*/
|
||||
const adjustSnapResult = (
|
||||
id: string,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
snapResult: SnapResult,
|
||||
): SnapResult => {
|
||||
const adjustPosition = adjustComponentPosition(id, x, y, width, height);
|
||||
console.log('adjustPosition', adjustPosition, 'x', x, 'y', y);
|
||||
console.log('currentComp', currentComp);
|
||||
|
||||
//* 如果调整值有负数,则直接返回原始坐标,不进行后续逻辑判断(暴力调整,暂不开启)
|
||||
// if (adjustPosition.x < 0 || adjustPosition.y < 0) {
|
||||
// snapResult.left = currentComp.x;
|
||||
// snapResult.top = currentComp.y;
|
||||
// return snapResult;
|
||||
// }
|
||||
|
||||
//* 重新赋值,但需要避免不能将组件移动到负坐标上
|
||||
if (adjustPosition.x !== x) {
|
||||
if (adjustPosition.x <= 0) {
|
||||
snapResult.left = x;
|
||||
} else if (adjustPosition.x + width >= parentWidth.value) {
|
||||
snapResult.left = parentWidth.value - width;
|
||||
} else {
|
||||
snapResult.left = adjustPosition.x;
|
||||
}
|
||||
snapResult.isSnap = true;
|
||||
} else if (x < 0) {
|
||||
snapResult.left = 0;
|
||||
// snapResult.left = currentComp.x;
|
||||
// snapResult.top = currentComp.y;
|
||||
snapResult.isSnap = true;
|
||||
}
|
||||
if (adjustPosition.y !== y) {
|
||||
snapResult.top = adjustPosition.y <= 0 ? 0 : adjustPosition.y;
|
||||
snapResult.left = adjustPosition.x;
|
||||
snapResult.isSnap = true;
|
||||
}
|
||||
|
||||
//* 返回前最后一次检查
|
||||
if (snapResult.left < 0 || snapResult.top < 0) {
|
||||
snapResult.left = currentComp.x;
|
||||
snapResult.top = currentComp.y;
|
||||
}
|
||||
|
||||
return snapResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算距离最近的组件
|
||||
* @param id
|
||||
* @param x
|
||||
* @param y
|
||||
* @param width
|
||||
* @param height
|
||||
*/
|
||||
const getClosestComponent = (id: string, x: number, y: number, width: number, height: number): ComponentState => {
|
||||
const otherComponents = components.filter((comp) => comp.id !== id);
|
||||
|
||||
const calculateEdgeDistance = (
|
||||
comp1: { x: number; y: number; width: number; height: number },
|
||||
comp2: { x: number; y: number; width: number; height: number },
|
||||
): number => {
|
||||
const left = Math.abs(comp1.x - (comp2.x + comp2.width));
|
||||
const right = Math.abs(comp1.x + comp1.width - comp2.x);
|
||||
const top = Math.abs(comp1.y - (comp2.y + comp2.height));
|
||||
const bottom = Math.abs(comp1.y + comp1.height - comp2.y);
|
||||
const minX = Math.min(left, right);
|
||||
const minY = Math.min(top, bottom);
|
||||
return Math.min(minX, minY);
|
||||
};
|
||||
|
||||
let closestComponent = otherComponents[0];
|
||||
let shortestDistance = calculateEdgeDistance({ x, y, width, height }, closestComponent);
|
||||
|
||||
for (const component of otherComponents) {
|
||||
const distance = calculateEdgeDistance({ x, y, width, height }, component);
|
||||
if (distance < shortestDistance) {
|
||||
shortestDistance = distance;
|
||||
closestComponent = component;
|
||||
}
|
||||
}
|
||||
|
||||
return closestComponent;
|
||||
};
|
||||
|
||||
const handleCollision = (id: string, x: number, y: number, width: number, height: number) => {
|
||||
//* 仅检测碰撞,不移动其他组件
|
||||
return detectCollision(id, x, y, width, height);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
parentWidth.value = document.querySelector('.app')!.clientWidth;
|
||||
parentHeight.value = document.querySelector('.app')!.clientHeight;
|
||||
loadState();
|
||||
});
|
||||
|
||||
/**
|
||||
* 是否与其他组件重叠
|
||||
* @param comp1
|
||||
* @param comp2
|
||||
*/
|
||||
const isOverlapping = (comp1: ComponentState, comp2: ComponentState): boolean => {
|
||||
return !(
|
||||
comp1.x >= comp2.x + comp2.width ||
|
||||
comp1.x + comp1.width <= comp2.x ||
|
||||
comp1.y >= comp2.y + comp2.height ||
|
||||
comp1.y + comp1.height <= comp2.y
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 调整当前组件位置
|
||||
* @param id
|
||||
* @param x
|
||||
* @param y
|
||||
* @param width
|
||||
* @param height
|
||||
*/
|
||||
const adjustComponentPosition = (
|
||||
id: string,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
): { x: number; y: number } => {
|
||||
const otherComponents = components.filter((comp) => comp.id !== id);
|
||||
let adjustedX = x;
|
||||
let adjustedY = y;
|
||||
|
||||
const adjustPosition = () => {
|
||||
let adjusted = false;
|
||||
for (const component of otherComponents) {
|
||||
if (isOverlapping({ id, x: adjustedX, y: adjustedY, width, height }, component)) {
|
||||
const right = component.x + component.width;
|
||||
const bottom = component.y + component.height;
|
||||
const leftOverlap = adjustedX < component.x;
|
||||
const rightOverlap = adjustedX + width > component.x + component.width;
|
||||
const topOverlap = adjustedY < component.y;
|
||||
const bottomOverlap = adjustedY + height > component.y + component.height;
|
||||
|
||||
if (leftOverlap) {
|
||||
console.log('leftOverlap');
|
||||
adjustedX = component.x - width;
|
||||
adjusted = true;
|
||||
} else if (rightOverlap) {
|
||||
console.log('rightOverlap', component);
|
||||
if (component.x + component.width + width > parentWidth.value) {
|
||||
console.log('rightOverlap overflow');
|
||||
adjustedX = currentComp.x;
|
||||
adjustedY = currentComp.y;
|
||||
}
|
||||
adjusted = true;
|
||||
} else if (topOverlap) {
|
||||
console.log('topOverlap');
|
||||
adjustedY = component.y - height;
|
||||
adjusted = true;
|
||||
} else if (bottomOverlap) {
|
||||
console.log('bottomOverlap');
|
||||
adjustedY = bottom;
|
||||
adjusted = true;
|
||||
} else {
|
||||
console.log('当前组件在其他组件内部');
|
||||
}
|
||||
|
||||
//* 检查是否还是与其他组件重叠
|
||||
let stillOverlapping = false;
|
||||
for (const comp of otherComponents) {
|
||||
if (isOverlapping({ id, x: adjustedX, y: adjustedY, width, height }, comp)) {
|
||||
stillOverlapping = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stillOverlapping) {
|
||||
//* 恢复组件移动前的原始位置
|
||||
console.log('stillOverlapping');
|
||||
adjustedX = currentComp.x;
|
||||
adjustedY = currentComp.y;
|
||||
adjusted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return adjusted;
|
||||
};
|
||||
|
||||
while (adjustPosition()) {
|
||||
const prevX = adjustedX;
|
||||
const prevY = adjustedY;
|
||||
if (adjustPosition()) {
|
||||
if (adjustedX === prevX && adjustedY === prevY) {
|
||||
console.log('full overlap');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { x: adjustedX, y: adjustedY };
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.app {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f0f0f0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
158
src/views/components/form/config.ts
Normal file
158
src/views/components/form/config.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import type { Form } from '@/components/wyg-form/interface';
|
||||
export const options = [
|
||||
{
|
||||
value: 'beijing',
|
||||
label: 'Beijing',
|
||||
children: [
|
||||
{
|
||||
value: 'chaoyang',
|
||||
label: 'ChaoYang',
|
||||
children: [
|
||||
{
|
||||
value: 'datunli',
|
||||
label: 'Datunli',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'haidian',
|
||||
label: 'Haidian',
|
||||
},
|
||||
{
|
||||
value: 'dongcheng',
|
||||
label: 'Dongcheng',
|
||||
},
|
||||
{
|
||||
value: 'xicheng',
|
||||
label: 'Xicheng',
|
||||
children: [
|
||||
{
|
||||
value: 'jinrongjie',
|
||||
label: 'Jinrongjie',
|
||||
},
|
||||
{
|
||||
value: 'tianqiao',
|
||||
label: 'Tianqiao',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'shanghai',
|
||||
label: 'Shanghai',
|
||||
children: [
|
||||
{
|
||||
value: 'huangpu',
|
||||
label: 'Huangpu',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const formConfig: Form.FieldItem[] = [
|
||||
{
|
||||
field: 'basic',
|
||||
label: '基本信息',
|
||||
component: 'title',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
label: '姓名',
|
||||
required: true,
|
||||
component: 'input',
|
||||
allowClear: true,
|
||||
colProps: {
|
||||
span: 20,
|
||||
offset: 100,
|
||||
},
|
||||
rules: [{ required: true, message: 'name is required' }],
|
||||
},
|
||||
{
|
||||
field: 'age',
|
||||
label: '年龄',
|
||||
required: true,
|
||||
component: 'input',
|
||||
allowClear: true,
|
||||
},
|
||||
{
|
||||
field: 'hobby',
|
||||
label: '爱好',
|
||||
required: true,
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'school',
|
||||
label: '学校',
|
||||
required: true,
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
label: '性别',
|
||||
required: true,
|
||||
component: 'select',
|
||||
lists: [
|
||||
{
|
||||
label: '男',
|
||||
value: 'M',
|
||||
},
|
||||
{
|
||||
label: '女',
|
||||
value: 'F',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'birthTime',
|
||||
label: '时间',
|
||||
component: 'time',
|
||||
},
|
||||
{
|
||||
field: 'birthDate',
|
||||
label: '出生日期',
|
||||
component: 'date',
|
||||
},
|
||||
{
|
||||
field: 'rangeDate',
|
||||
label: '日期范围',
|
||||
component: 'rangeDate',
|
||||
},
|
||||
{
|
||||
field: 'address',
|
||||
label: '地址',
|
||||
component: 'cascader',
|
||||
options,
|
||||
},
|
||||
{
|
||||
field: 'desc',
|
||||
label: '自我介绍',
|
||||
required: true,
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'contact',
|
||||
label: '联系信息',
|
||||
component: 'title',
|
||||
},
|
||||
{
|
||||
field: 'tel',
|
||||
label: '联系电话',
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: '邮箱',
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'relation',
|
||||
label: '关系信息',
|
||||
component: 'title',
|
||||
},
|
||||
{
|
||||
field: 'relationTable',
|
||||
label: '关系信息',
|
||||
component: 'slot',
|
||||
},
|
||||
];
|
||||
67
src/views/components/form/index.vue
Normal file
67
src/views/components/form/index.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<a-card class="mb-5">
|
||||
<eo-form ref="formRef" :fieldList="fieldList" :model="model" @submit="handleSubmit" @change="handleChange">
|
||||
<!-- <template #header> 基本表单header </template> -->
|
||||
<!-- <template #footer>基本表单footer</template> -->
|
||||
|
||||
<!-- <template #buttons="{ model, formRef }">
|
||||
<a-button @click="handleSubmit(model, formRef)">提交</a-button>
|
||||
</template> -->
|
||||
|
||||
<!-- <template #relationTable>
|
||||
<div>关系人slot</div>
|
||||
</template> -->
|
||||
</eo-form>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="onSubmit">父组件提交</a-button>
|
||||
<a-button @click="onSwith">启用/禁用</a-button>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import EoForm from '@/components/wyg-form/index.vue';
|
||||
import { formConfig } from './config';
|
||||
import type { Form } from '@/components/wyg-form/interface';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
const formRef = ref();
|
||||
|
||||
let fieldList: Form.FieldItem[] = formConfig;
|
||||
const model = ref<Record<string, any>>({
|
||||
name: '张三',
|
||||
age: 18,
|
||||
});
|
||||
// 提交按钮在子组件
|
||||
const handleSubmit = (model: Record<string, any>) => {
|
||||
console.log(model, 'model====');
|
||||
};
|
||||
// 提交按钮在父组件
|
||||
const onSubmit = async () => {
|
||||
const res = await formRef?.value?.submit();
|
||||
console.log(formRef, 'formRef====');
|
||||
console.log(res, 'res====');
|
||||
};
|
||||
// 修改Form组件相关属性
|
||||
const status = ref(false);
|
||||
const onSwith = () => {
|
||||
status.value = !status.value;
|
||||
formRef?.value?.setForm({ disabled: !status.value });
|
||||
};
|
||||
|
||||
const handleChange = (params: any) => {
|
||||
const { key, val } = params;
|
||||
if (key === 'sex') {
|
||||
const required = val === 'F';
|
||||
// 更新form配置
|
||||
const updateList: any = [
|
||||
{
|
||||
field: 'school',
|
||||
required: required,
|
||||
disabled: !required,
|
||||
},
|
||||
];
|
||||
formRef?.value?.updateFieldsList(updateList);
|
||||
formRef?.value?.setModel({ age: 26 });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
39
src/views/components/layout/comp1.vue
Normal file
39
src/views/components/layout/comp1.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<!--
|
||||
* @Author: 田鑫
|
||||
* @Date: 2024-06-07 13:42:33
|
||||
* @LastEditors: 田鑫
|
||||
* @LastEditTime: 2024-06-07 18:27:25
|
||||
* @Description:
|
||||
-->
|
||||
<template>
|
||||
<div id="comp1">
|
||||
comp1
|
||||
<!-- <div class="flex-item">comp 1</div>
|
||||
<div class="flex-item">comp 1</div>
|
||||
<div class="flex-item">comp 1</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.flex-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.flex-item {
|
||||
background-color: #4caf50;
|
||||
padding: 20px;
|
||||
margin: 10px;
|
||||
color: white;
|
||||
font-size: 1.5em;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
13
src/views/components/layout/comp2.vue
Normal file
13
src/views/components/layout/comp2.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<!--
|
||||
* @Author: 田鑫
|
||||
* @Date: 2024-06-07 13:55:17
|
||||
* @LastEditors: 田鑫
|
||||
* @LastEditTime: 2024-06-07 18:27:29
|
||||
* @Description:
|
||||
-->
|
||||
<template>
|
||||
<div id="comp2">comp2</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<style lang="less" scoped></style>
|
||||
13
src/views/components/layout/comp3.vue
Normal file
13
src/views/components/layout/comp3.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<!--
|
||||
* @Author: 田鑫
|
||||
* @Date: 2024-06-07 13:55:17
|
||||
* @LastEditors: 田鑫
|
||||
* @LastEditTime: 2024-06-07 18:27:32
|
||||
* @Description:
|
||||
-->
|
||||
<template>
|
||||
<div id="comp3">comp3</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<style lang="less" scoped></style>
|
||||
13
src/views/components/layout/comp4.vue
Normal file
13
src/views/components/layout/comp4.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<!--
|
||||
* @Author: 田鑫
|
||||
* @Date: 2024-06-07 13:55:17
|
||||
* @LastEditors: 田鑫
|
||||
* @LastEditTime: 2024-06-07 18:27:35
|
||||
* @Description:
|
||||
-->
|
||||
<template>
|
||||
<div id="comp4">comp4</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<style lang="less" scoped></style>
|
||||
408
src/views/components/layout/index.vue
Normal file
408
src/views/components/layout/index.vue
Normal file
@ -0,0 +1,408 @@
|
||||
<template>
|
||||
<div class="page-container" ref="container">
|
||||
<div class="layout-buttons">
|
||||
<button @click="switchLayout('strategy1')">默认布局</button>
|
||||
<button @click="switchLayout('strategy2')">策略2布局</button>
|
||||
<button @click="switchLayout('strategy3')">策略3布局</button>
|
||||
</div>
|
||||
|
||||
<div>屏幕宽度:{{ screenWidth }}px</div>
|
||||
|
||||
<div class="components-container">
|
||||
<component
|
||||
v-for="component in components"
|
||||
:is="component.comp"
|
||||
:key="component.id"
|
||||
class="component"
|
||||
:style="getComponentStyle(component)"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import Comp1 from './comp1.vue';
|
||||
import Comp2 from './comp2.vue';
|
||||
import Comp3 from './comp3.vue';
|
||||
import Comp4 from './comp4.vue';
|
||||
|
||||
interface ComponentState {
|
||||
id: string;
|
||||
comp: ReturnType<typeof defineComponent>;
|
||||
x: number | string;
|
||||
y: number | string;
|
||||
width?: number | string;
|
||||
minWidth?: number | string;
|
||||
maxWidth?: number | string;
|
||||
height: number;
|
||||
zIndex?: number | string;
|
||||
background?: string;
|
||||
}
|
||||
|
||||
const components = ref<ComponentState[]>([]);
|
||||
const container = ref<HTMLElement | null>(null);
|
||||
const screenWidth = ref(0);
|
||||
const screenHeight = ref(0);
|
||||
const layoutComp1 = shallowRef(Comp1);
|
||||
const layoutComp2 = shallowRef(Comp2);
|
||||
const layoutComp3 = shallowRef(Comp3);
|
||||
const layoutComp4 = shallowRef(Comp4);
|
||||
|
||||
const layoutCompMap = {
|
||||
comp1: layoutComp1,
|
||||
comp2: layoutComp2,
|
||||
comp3: layoutComp3,
|
||||
comp4: layoutComp4,
|
||||
};
|
||||
|
||||
const containerWidth = ref(0);
|
||||
const containerHeight = ref(0);
|
||||
const currentLayoutStrategy = ref<LayoutStrategy>('strategy1');
|
||||
|
||||
onMounted(() => {
|
||||
const observedElement = document.querySelector('#app');
|
||||
if (observedElement) {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
const { width, height } = entry.contentRect;
|
||||
screenWidth.value = width;
|
||||
screenHeight.value = height;
|
||||
|
||||
console.log('screenWidth', width);
|
||||
|
||||
const compStyles = loadComponentWidthRange(currentLayoutStrategy.value, width);
|
||||
if (compStyles) {
|
||||
setComponentStyleByStrategy(compStyles);
|
||||
}
|
||||
// const screenResolution = getComponentWidthRange(width);
|
||||
// if (screenResolution) {
|
||||
// setComponentStyleByStrategy(screenResolution.compStyles);
|
||||
// }
|
||||
});
|
||||
resizeObserver.observe(observedElement);
|
||||
}
|
||||
});
|
||||
|
||||
type Coordinate = number | [number, number];
|
||||
|
||||
type ComponentWidthRange = {
|
||||
width: [number, number];
|
||||
height: number;
|
||||
compName: string;
|
||||
background?: string;
|
||||
x: Coordinate;
|
||||
y: Coordinate;
|
||||
};
|
||||
|
||||
type ScreenResolutionMap = {
|
||||
[resolutionRange: string]: {
|
||||
layoutStrategy: string;
|
||||
compStyles: ComponentWidthRange[];
|
||||
};
|
||||
};
|
||||
|
||||
type LayoutStrategy = 'strategy1' | 'strategy2' | 'strategy3';
|
||||
|
||||
type Resolution = {
|
||||
[resolutionRange: string]: ComponentWidthRange[];
|
||||
};
|
||||
|
||||
type ScreenResolutionMap1 = {
|
||||
layoutStrategy: LayoutStrategy;
|
||||
resolution: Resolution;
|
||||
};
|
||||
|
||||
const screenResolutionMap1: ScreenResolutionMap1[] = [
|
||||
{
|
||||
layoutStrategy: 'strategy1',
|
||||
resolution: {
|
||||
'[0, 768]': [
|
||||
{ width: [498, 498], compName: 'comp1', height: 550, background: 'red', x: 266, y: 0 },
|
||||
{ width: [498, 498], compName: 'comp2', height: 304, background: '#ccdc', x: [266, 266], y: 550 },
|
||||
{ width: [266, 266], compName: 'comp3', height: 856, background: '#5E6673', x: 0, y: 0 },
|
||||
{ width: [768, 768], compName: 'comp4', height: 304, background: 'blue', x: 0, y: 856 },
|
||||
],
|
||||
'[769, 1280]': [
|
||||
{ width: [498, 836], compName: 'comp1', height: 610, background: 'red', x: 0, y: 0 },
|
||||
{ width: [135, 222], compName: 'comp2', height: 610, background: '#ccdc', x: [498, 836], y: 0 },
|
||||
{ width: [135, 222], compName: 'comp3', height: 908, background: '#5E6673', x: [768, 1012], y: 0 },
|
||||
{ width: [768, 1012], compName: 'comp4', height: 356, background: 'blue', x: 0, y: 610 },
|
||||
],
|
||||
'[1281, 1440]': [
|
||||
{ width: [836, 836], compName: 'comp1', height: 610, background: 'red', x: 0, y: 0 },
|
||||
{ width: [222, 302], compName: 'comp2', height: 610, background: '#ccdc', x: 836, y: 0 },
|
||||
{ width: [222, 302], compName: 'comp3', height: 908, background: '#5E6673', x: [1058, 1138], y: 0 },
|
||||
{ width: [1012, 1138], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 610 },
|
||||
],
|
||||
'[1441, 1920]': [
|
||||
{ width: [836, 1196], compName: 'comp1', height: 610, background: 'red', x: 0, y: 0 },
|
||||
{ width: [302, 362], compName: 'comp2', height: 610, background: '#ccdc', x: [836, 1196], y: 0 },
|
||||
{ width: [302, 362], compName: 'comp3', height: 908, background: '#5E6673', x: [1138, 1158], y: 0 },
|
||||
{ width: [1138, 1558], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 610 },
|
||||
],
|
||||
'[1921, 3860]': [
|
||||
{ width: [1196, 1696], compName: 'comp1', height: 610, background: 'red', x: 0, y: 0 },
|
||||
{ width: [362, 432], compName: 'comp2', height: 610, background: '#ccdc', x: [1196, 1696], y: 0 },
|
||||
{ width: [362, 432], compName: 'comp3', height: 908, background: '#5E6673', x: [1558, 2128], y: 0 },
|
||||
{ width: [1558, 2128], compName: 'comp4', height: 356, background: 'blue', x: 0, y: 610 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
layoutStrategy: 'strategy2',
|
||||
resolution: {
|
||||
'[0, 768]': [
|
||||
{ width: [200, 300], compName: 'comp1', height: 500, background: 'red', x: 0, y: 0 },
|
||||
{ width: [300, 400], compName: 'comp2', height: 500, background: '#ccdc', x: [200, 300], y: 0 },
|
||||
{ width: [300, 400], compName: 'comp3', height: 970, background: '#5E6673', x: [500, 700], y: 0 },
|
||||
{ width: [300, 400], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 500 },
|
||||
],
|
||||
'[769, 1280]': [
|
||||
{ width: [200, 300], compName: 'comp1', height: 500, background: 'red', x: 0, y: 0 },
|
||||
{ width: [300, 400], compName: 'comp2', height: 500, background: '#ccdc', x: [200, 300], y: 0 },
|
||||
{ width: [300, 400], compName: 'comp3', height: 970, background: '#5E6673', x: [500, 700], y: 0 },
|
||||
{ width: [300, 400], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 500 },
|
||||
],
|
||||
'[1281, 1440]': [
|
||||
{ width: [280, 400], compName: 'comp1', height: 600, background: 'red', x: 0, y: 0 },
|
||||
{ width: [350, 450], compName: 'comp2', height: 500, background: '#ccdc', x: [280, 400], y: 0 },
|
||||
{ width: [350, 450], compName: 'comp3', height: 970, background: '#5E6673', x: [630, 850], y: 0 },
|
||||
{ width: [350, 450], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 600 },
|
||||
],
|
||||
'[1441, 1920]': [
|
||||
{ width: [320, 420], compName: 'comp1', height: 500, background: 'red', x: 0, y: 0 },
|
||||
{ width: [400, 500], compName: 'comp2', height: 500, background: '#ccdc', x: [320, 420], y: 0 },
|
||||
{ width: [400, 500], compName: 'comp3', height: 970, background: '#5E6673', x: [720, 920], y: 0 },
|
||||
{ width: [400, 500], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 500 },
|
||||
],
|
||||
'[1921, 3860]': [
|
||||
{ width: [800, 1000], compName: 'comp1', height: 600, background: 'red', x: 0, y: 0 },
|
||||
{ width: [500, 600], compName: 'comp2', height: 600, background: '#ccdc', x: [800, 1000], y: 0 },
|
||||
{ width: [500, 600], compName: 'comp3', height: 1070, background: '#5E6673', x: [1300, 1600], y: 0 },
|
||||
{ width: [500, 600], compName: 'comp4', height: 510, background: 'blue', x: 0, y: 600 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
layoutStrategy: 'strategy3',
|
||||
resolution: {
|
||||
'[0, 768]': [
|
||||
{ width: [200, 300], compName: 'comp1', height: 500, background: 'red', x: 0, y: 0 },
|
||||
{ width: [300, 400], compName: 'comp2', height: 500, background: '#ccdc', x: [200, 300], y: 0 },
|
||||
{ width: [300, 400], compName: 'comp3', height: 970, background: '#5E6673', x: [500, 700], y: 0 },
|
||||
{ width: [300, 400], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 500 },
|
||||
],
|
||||
'[769, 1280]': [
|
||||
{ width: [200, 300], compName: 'comp1', height: 500, background: 'red', x: 0, y: 0 },
|
||||
{ width: [300, 400], compName: 'comp2', height: 500, background: '#ccdc', x: [200, 300], y: 0 },
|
||||
{ width: [300, 400], compName: 'comp3', height: 970, background: '#5E6673', x: [500, 700], y: 0 },
|
||||
{ width: [300, 400], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 500 },
|
||||
],
|
||||
'[1281, 1440]': [
|
||||
{ width: [280, 400], compName: 'comp1', height: 600, background: 'red', x: 0, y: 0 },
|
||||
{ width: [350, 450], compName: 'comp2', height: 500, background: '#ccdc', x: [280, 400], y: 0 },
|
||||
{ width: [350, 450], compName: 'comp3', height: 970, background: '#5E6673', x: [630, 850], y: 0 },
|
||||
{ width: [350, 450], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 600 },
|
||||
],
|
||||
'[1441, 1920]': [
|
||||
{ width: [320, 420], compName: 'comp1', height: 500, background: 'red', x: 0, y: 0 },
|
||||
{ width: [400, 500], compName: 'comp2', height: 500, background: '#ccdc', x: [320, 420], y: 0 },
|
||||
{ width: [400, 500], compName: 'comp3', height: 970, background: '#5E6673', x: [720, 920], y: 0 },
|
||||
{ width: [400, 500], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 500 },
|
||||
],
|
||||
'[1921, 3860]': [
|
||||
{ width: [800, 1000], compName: 'comp1', height: 600, background: 'red', x: 0, y: 0 },
|
||||
{ width: [500, 600], compName: 'comp2', height: 600, background: '#ccdc', x: [800, 1000], y: 0 },
|
||||
{ width: [500, 600], compName: 'comp3', height: 1070, background: '#5E6673', x: [1300, 1600], y: 0 },
|
||||
{ width: [500, 600], compName: 'comp4', height: 510, background: 'blue', x: 0, y: 600 },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 创建屏幕分辨率宽度到组件宽度区间的映射表
|
||||
const screenResolutionMap: ScreenResolutionMap = {
|
||||
'[1024, 1280]': {
|
||||
layoutStrategy: 'strategy1',
|
||||
compStyles: [
|
||||
{ width: [200, 300], compName: 'comp1', height: 500, background: 'red', x: 0, y: 0 },
|
||||
{ width: [300, 400], compName: 'comp2', height: 500, background: '#ccdc', x: [200, 300], y: 0 },
|
||||
{ width: [300, 400], compName: 'comp3', height: 970, background: '#5E6673', x: [500, 700], y: 0 },
|
||||
{ width: [300, 400], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 500 },
|
||||
],
|
||||
},
|
||||
'[1281, 1440]': {
|
||||
layoutStrategy: 'strategy2',
|
||||
compStyles: [
|
||||
{ width: [280, 400], compName: 'comp1', height: 600, background: 'red', x: 0, y: 0 },
|
||||
{ width: [350, 450], compName: 'comp2', height: 500, background: '#ccdc', x: [280, 400], y: 0 },
|
||||
{ width: [350, 450], compName: 'comp3', height: 970, background: '#5E6673', x: [630, 850], y: 0 },
|
||||
{ width: [350, 450], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 600 },
|
||||
],
|
||||
},
|
||||
'[1441, 1920]': {
|
||||
layoutStrategy: 'strategy3',
|
||||
compStyles: [
|
||||
{ width: [320, 420], compName: 'comp1', height: 500, background: 'red', x: [400, 500], y: 0 },
|
||||
{ width: [400, 500], compName: 'comp2', height: 500, background: '#ccdc', x: 0, y: 0 },
|
||||
{ width: [400, 500], compName: 'comp3', height: 970, background: '#5E6673', x: [720, 920], y: 0 },
|
||||
{ width: [400, 500], compName: 'comp4', height: 410, background: 'blue', x: 0, y: 500 },
|
||||
],
|
||||
},
|
||||
'[1921, 3860]': {
|
||||
layoutStrategy: 'strategy4',
|
||||
compStyles: [
|
||||
{ width: [800, 1000], compName: 'comp1', height: 600, background: 'red', x: 0, y: 0 },
|
||||
{ width: [500, 600], compName: 'comp2', height: 600, background: '#ccdc', x: [800, 1000], y: 0 },
|
||||
{ width: [500, 600], compName: 'comp3', height: 1070, background: '#5E6673', x: [1300, 1600], y: 0 },
|
||||
{ width: [500, 600], compName: 'comp4', height: 510, background: 'blue', x: 0, y: 600 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function getComponentWidthRange(screenWidth: number): ScreenResolutionMap | null {
|
||||
for (const resolutionRange in screenResolutionMap) {
|
||||
const [minWidth, maxWidth] = JSON.parse(resolutionRange.replace('[', '[').replace(']', ']'));
|
||||
if (screenWidth >= minWidth && screenWidth <= maxWidth) {
|
||||
return screenResolutionMap[resolutionRange];
|
||||
}
|
||||
}
|
||||
return null; // 如果没有找到对应的分辨率范围,则返回null
|
||||
}
|
||||
|
||||
const getComponentWidthRange1 = (screenWidth: number, resolution: Resolution): ComponentWidthRange[] | null => {
|
||||
for (const resolutionRange in resolution) {
|
||||
const [minWidth, maxWidth] = JSON.parse(resolutionRange.replace('[', '[').replace(']', ']'));
|
||||
if (screenWidth >= minWidth && screenWidth <= maxWidth) {
|
||||
return resolution[resolutionRange];
|
||||
}
|
||||
}
|
||||
return null; // 如果没有找到对应的分辨率范围,则返回null
|
||||
};
|
||||
|
||||
const loadComponentWidthRange = (layoutStrategy: LayoutStrategy, screenWidth: number): ComponentWidthRange[] => {
|
||||
let layoutStyles: ComponentWidthRange[] | null = [];
|
||||
for (let item of screenResolutionMap1) {
|
||||
console.log('layoutStrategy', item.layoutStrategy);
|
||||
if (item.layoutStrategy === layoutStrategy) {
|
||||
layoutStyles = getComponentWidthRange1(screenWidth, item.resolution);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!layoutStyles || layoutStyles.length === 0) {
|
||||
layoutStyles = getComponentWidthRange1(screenWidth, screenResolutionMap1[0].resolution) as ComponentWidthRange[];
|
||||
}
|
||||
return layoutStyles;
|
||||
};
|
||||
|
||||
const setComponentStyleByStrategy = (compStyles: ComponentWidthRange[]) => {
|
||||
components.value = compStyles.map((item) => {
|
||||
const compPosition = getRelativePosition(item, screenWidth.value);
|
||||
return {
|
||||
id: item.compName,
|
||||
comp: layoutCompMap[item.compName],
|
||||
x: compPosition.x,
|
||||
y: item.y,
|
||||
width: compPosition.width,
|
||||
height: item.height,
|
||||
zIndex: 1,
|
||||
background: item.background,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算组件相对位置和大小的函数
|
||||
* @param compStyles - 组件的样式信息
|
||||
* @param screenWidth - 屏幕宽度
|
||||
* @returns 组件的计算位置和大小
|
||||
*/
|
||||
function getRelativePosition(compStyles: ComponentWidthRange, screenWidth: number): { x: string; width: string } {
|
||||
const { x, width } = compStyles;
|
||||
|
||||
// 解析区间值
|
||||
const parseRange = (value: Coordinate): { min: number; max: number } => {
|
||||
if (Array.isArray(value)) {
|
||||
const [min, max] = value;
|
||||
return { min, max };
|
||||
}
|
||||
return { min: value, max: value };
|
||||
};
|
||||
|
||||
// 转换为基于屏幕宽度的百分比
|
||||
const toPercentage = (
|
||||
range: { min: number; max: number },
|
||||
screenWidth: number,
|
||||
): { minPercentage: number; maxPercentage: number } => {
|
||||
const minPercentage = (range.min / screenWidth) * 100;
|
||||
const maxPercentage = (range.max / screenWidth) * 100;
|
||||
return { minPercentage, maxPercentage };
|
||||
};
|
||||
|
||||
// 解析并计算宽度和x值
|
||||
const widthRange = parseRange(width);
|
||||
const xRange = parseRange(x);
|
||||
|
||||
const widthPercentage = toPercentage(widthRange, screenWidth);
|
||||
const xPercentage = toPercentage(xRange, screenWidth);
|
||||
|
||||
// 假设我们取区间的中间值作为最终值
|
||||
const finalWidth = ((widthPercentage.minPercentage + widthPercentage.maxPercentage) / 2).toFixed(2);
|
||||
const finalX = ((xPercentage.minPercentage + xPercentage.maxPercentage) / 2).toFixed(2);
|
||||
|
||||
return {
|
||||
x: finalX,
|
||||
width: finalWidth,
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (container.value) {
|
||||
containerWidth.value = container.value.clientWidth;
|
||||
containerHeight.value = container.value.clientHeight;
|
||||
}
|
||||
});
|
||||
|
||||
const switchLayout = (layoutStrategy: LayoutStrategy) => {};
|
||||
|
||||
function getComponentStyle(component: ComponentState) {
|
||||
return {
|
||||
position: 'absolute',
|
||||
left: `${component.x}%`,
|
||||
top: `${component.y}px`,
|
||||
width: `${component.width}%`,
|
||||
height: `${component.height}px`,
|
||||
zIndex: component.zIndex,
|
||||
background: component.background,
|
||||
transition: 'all 0.25s ease',
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.page-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
.layout-buttons {
|
||||
width: 100%;
|
||||
height: 5%;
|
||||
}
|
||||
.components-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 95%;
|
||||
border: 1px solid #ccc;
|
||||
overflow: hidden;
|
||||
.component {
|
||||
background-color: lightgray;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.25s ease;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
src/views/components/login/index.vue
Normal file
3
src/views/components/login/index.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<view>login页面</view>
|
||||
</template>
|
||||
34
src/views/components/permission/auth.vue
Normal file
34
src/views/components/permission/auth.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<!--
|
||||
* @Author: 田鑫
|
||||
* @Date: 2023-03-05 15:13:44
|
||||
* @LastEditors: 田鑫
|
||||
* @LastEditTime: 2023-03-05 15:43:18
|
||||
* @Description: 模拟登录鉴权页
|
||||
-->
|
||||
|
||||
<template>
|
||||
<a-modal title="登录鉴权页" :visible="true" :footer="false">
|
||||
<div w100 align-center>
|
||||
<a-button w-160 @click="login" :loading="loading" type="primary">登录</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
|
||||
function login() {
|
||||
loading.value = true;
|
||||
localStorage.setItem('satoken', '123asdzxc');
|
||||
AMessage.success('登录鉴权成功,准备跳转');
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
router.push({ name: route.query?.redirect ? route.query?.redirect : 'dashboard' });
|
||||
}, 1500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
19
src/views/components/permission/choose-enterprise.vue
Normal file
19
src/views/components/permission/choose-enterprise.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<!--
|
||||
* @Author: 田鑫
|
||||
* @Date: 2023-03-05 14:27:21
|
||||
* @LastEditors: 田鑫
|
||||
* @LastEditTime: 2023-03-05 15:14:15
|
||||
* @Description:
|
||||
-->
|
||||
<template>
|
||||
<a-modal title="选择企业:" :visible="true">
|
||||
<a-select v-model="enterprise" placeholder="请选择您的企业"></a-select>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const enterprise = ref('');
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
38
src/views/components/table/columns.ts
Normal file
38
src/views/components/table/columns.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* @Author: 田鑫
|
||||
* @Date: 2023-02-20 14:00:07
|
||||
* @LastEditors: 田鑫
|
||||
* @LastEditTime: 2023-02-20 14:00:08
|
||||
* @Description:
|
||||
*/
|
||||
import type { TableColumnData } from '@arco-design/web-vue';
|
||||
|
||||
export const columnsData: TableColumnData[] = [
|
||||
{
|
||||
title: '测试表头1',
|
||||
dataIndex: 'column1',
|
||||
},
|
||||
{
|
||||
title: '测试表头2',
|
||||
dataIndex: 'column2',
|
||||
},
|
||||
{
|
||||
title: '测试表头3',
|
||||
dataIndex: 'column3',
|
||||
},
|
||||
{
|
||||
title: '测试表头4',
|
||||
dataIndex: 'column4',
|
||||
},
|
||||
{
|
||||
title: '测试表头5',
|
||||
dataIndex: 'column5',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: '',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
slotName: 'opeartion',
|
||||
},
|
||||
];
|
||||
132
src/views/components/table/config.ts
Normal file
132
src/views/components/table/config.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import type { Form } from '../wyg-form/interface';
|
||||
export const options = [
|
||||
{
|
||||
value: 'beijing',
|
||||
label: 'Beijing',
|
||||
children: [
|
||||
{
|
||||
value: 'chaoyang',
|
||||
label: 'ChaoYang',
|
||||
children: [
|
||||
{
|
||||
value: 'datunli',
|
||||
label: 'Datunli',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'haidian',
|
||||
label: 'Haidian',
|
||||
},
|
||||
{
|
||||
value: 'dongcheng',
|
||||
label: 'Dongcheng',
|
||||
},
|
||||
{
|
||||
value: 'xicheng',
|
||||
label: 'Xicheng',
|
||||
children: [
|
||||
{
|
||||
value: 'jinrongjie',
|
||||
label: 'Jinrongjie',
|
||||
},
|
||||
{
|
||||
value: 'tianqiao',
|
||||
label: 'Tianqiao',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'shanghai',
|
||||
label: 'Shanghai',
|
||||
children: [
|
||||
{
|
||||
value: 'huangpu',
|
||||
label: 'Huangpu',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const formConfig: Form.FieldItem[] = [
|
||||
{
|
||||
field: 'name',
|
||||
label: '姓名',
|
||||
required: true,
|
||||
value: '波波',
|
||||
component: 'input',
|
||||
allowClear: true,
|
||||
colProps: {
|
||||
span: 20,
|
||||
offset: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'age',
|
||||
label: '年龄',
|
||||
required: true,
|
||||
value: '18',
|
||||
component: 'input',
|
||||
allowClear: true,
|
||||
},
|
||||
{
|
||||
field: 'hobby',
|
||||
label: '爱好',
|
||||
required: true,
|
||||
value: '',
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'school',
|
||||
label: '学校',
|
||||
required: true,
|
||||
value: '',
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
label: '性别',
|
||||
required: true,
|
||||
component: 'select',
|
||||
lists: [
|
||||
{
|
||||
label: '男',
|
||||
value: 'M',
|
||||
},
|
||||
{
|
||||
label: '女',
|
||||
value: 'F',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'birthTime',
|
||||
label: '时间',
|
||||
component: 'time',
|
||||
},
|
||||
{
|
||||
field: 'birthDate',
|
||||
label: '出生日期',
|
||||
component: 'date',
|
||||
},
|
||||
{
|
||||
field: 'rangeDate',
|
||||
label: '日期范围',
|
||||
component: 'rangeDate',
|
||||
},
|
||||
{
|
||||
field: 'address',
|
||||
label: '地址',
|
||||
component: 'cascader',
|
||||
options,
|
||||
},
|
||||
{
|
||||
field: 'desc',
|
||||
label: '自我介绍',
|
||||
required: true,
|
||||
value: '',
|
||||
component: 'input',
|
||||
},
|
||||
];
|
||||
27
src/views/components/table/index.md
Normal file
27
src/views/components/table/index.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Table 说明
|
||||
|
||||
## 说明
|
||||
|
||||
`<a-table v-bind="propsRes" v-on="propsEvent" ></a-table>`
|
||||
|
||||
`复写a-table中原始的v-bind及v-on属性,propRes负责处理属性,propsEvent负责处理a-table的事件`
|
||||
|
||||
`setProps:设置a-table的属性,类型为:IDefaultProps`
|
||||
|
||||
`setColumns:设置table中columns的属性,类型为:TableColumnData[]`
|
||||
|
||||
`setLoadListParams: 设置列表数据接口的请求参数,类型为自定义`
|
||||
|
||||
`loadTableData:获取列表数据,返回列表参数`
|
||||
|
||||
## 使用方式
|
||||
|
||||
`<a-table v-bind="propsRes" v-on="propsEvent">`
|
||||
<br>
|
||||
`<template #xx></template> // 自定义插槽名称,在setColumns的传入值中设置`
|
||||
<br>
|
||||
`</a-table>`
|
||||
|
||||
## 示例
|
||||
|
||||
`具体参照table文件夹的示例`
|
||||
52
src/views/components/table/index.vue
Normal file
52
src/views/components/table/index.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<!--
|
||||
* @Author: 田鑫
|
||||
* @Date: 2023-02-20 13:58:48
|
||||
* @LastEditors: 田鑫
|
||||
* @LastEditTime: 2023-02-28 12:05:03
|
||||
* @Description: table示例
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-table v-bind="propsRes" v-on="propsEvent">
|
||||
<template #opeartion="{ record }">
|
||||
<div flex>
|
||||
<a-link>编辑</a-link>
|
||||
<ConfirmButton content="确定要删除该条数据吗?" popupType="error" @confirm-emit="deleteItem(record)">
|
||||
<a-link>删除</a-link>
|
||||
</ConfirmButton>
|
||||
<a-link>下载</a-link>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchTableData, type IExample } from '@/api/example';
|
||||
import useTableProps from '@/hooks/table-hooks';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { columnsData } from './columns';
|
||||
|
||||
const { propsRes, propsEvent, setProps, setColumns, setLoadListParams, loadTableData } =
|
||||
useTableProps<IExample.ITableResponse>(fetchTableData);
|
||||
setProps({
|
||||
'row-key': 'id',
|
||||
'row-selection': {
|
||||
type: 'checkbox',
|
||||
showCheckedAll: true,
|
||||
},
|
||||
'selected-keys': [1, 2, 3],
|
||||
});
|
||||
setColumns(columnsData);
|
||||
onMounted(async () => {
|
||||
await loadTableData();
|
||||
});
|
||||
|
||||
function add() {
|
||||
Message.info('=sa');
|
||||
}
|
||||
|
||||
function deleteItem(record) {}
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
50
src/views/components/workplace/index.vue
Normal file
50
src/views/components/workplace/index.vue
Normal 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>
|
||||
190
src/views/components/workplace/modules/case.vue
Normal file
190
src/views/components/workplace/modules/case.vue
Normal 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>
|
||||
29
src/views/components/workplace/modules/container.vue
Normal file
29
src/views/components/workplace/modules/container.vue
Normal 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>
|
||||
249
src/views/components/workplace/modules/product.vue
Normal file
249
src/views/components/workplace/modules/product.vue
Normal 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>
|
||||
Reference in New Issue
Block a user