WanGxC il y a 9 mois
Parent
commit
52f678e4e6

+ 78 - 82
src/views/product-manage/comment-detail/component/DataTable.vue

@@ -5,36 +5,33 @@
  * @Author: Cheney
  */
 
-import { Delete, Download, InfoFilled, Plus, Refresh, RefreshRight, Search, Upload } from '@element-plus/icons-vue';
+import { Download, InfoFilled, Plus, Refresh, RefreshRight, Search } from '@element-plus/icons-vue';
 import { ElMessage } from 'element-plus';
 import { usePagination } from '/@/utils/usePagination';
 import { useTableData } from '/@/utils/useTableData';
-import { useResponse } from '/@/utils/useResponse';
 import { CompetitorMonitorCommentColumns } from '/@/views/product-manage/Columns';
 import DataTableSlot from './DataTableSlot.vue';
 import PermissionButton from '/src/components/PermissionButton/index.vue';
-import ImportButton from '/src/components/ImportButton/index.vue';
 import VerticalDivider from '/src/components/VerticalDivider/index.vue';
 import * as api from '../api';
-import { downloadFile } from '/@/utils/service';
-import { getTableData } from '../api';
 import CreateDialog from '/@/views/product-manage/comment-detail/component/CreateDialog.vue';
 import { DictionaryStore } from '/@/stores/dictionary';
 import { useScoreEnum, useTivEnum } from '/@/views/product-manage/comment-detail/enum';
 import NegativeLabel from '/@/views/product-manage/comment-detail/component/NegativeLabel.vue';
 
+
 const { data: staticData } = DictionaryStore();
 
 const btnLoading = ref(false);
 
 const formInline = reactive<any>({
-	country: '',
-	score: '',
-	tiv: ''
+  country: '',
+  score: '',
+  tiv: ''
 });
 
 const props = defineProps({
-	asin: String
+  asin: String
 });
 const { asin } = props;
 const { tableOptions, handlePageChange } = usePagination(fetchList);
@@ -50,12 +47,12 @@ const gridOptions: any = reactive({
   showHeader: true,
   currentRowHighLight: true,
   height: 650,
-	// customConfig: {
-	// 	storage: {
-	// 		visible: true,
-	// 		resizable:false,
-	// 	}
-	// },
+  // customConfig: {
+  // 	storage: {
+  // 		visible: true,
+  // 		resizable:false,
+  // 	}
+  // },
   toolbarConfig: {
     size: 'large',
     // custom: true,
@@ -99,10 +96,10 @@ async function fetchList() {
   gridOptions.columns = [];
 
   const query = {
-		asin: asin,
-		country_code:formInline?.country,
-		score: formInline?.score,
-		tiv: formInline?.tiv,
+    asin: asin,
+    country_code: formInline?.country,
+    score: formInline?.score,
+    tiv: formInline?.tiv
   };
   await useTableData(api.getTableData, query, gridOptions);
   await gridRef.value.loadColumn(CompetitorMonitorCommentColumns);
@@ -117,10 +114,10 @@ async function handleDownload() {
   gridOptions.loading = true;
   try {
     const query = {
-			asin: asin,
-			country_code:formInline?.country,
-			score: formInline?.score,
-			tiv: formInline?.tiv,
+      asin: asin,
+      country_code: formInline?.country,
+      score: formInline?.score,
+      tiv: formInline?.tiv
     };
     const response = await api.exportData(query);
     const url = window.URL.createObjectURL(new Blob([ response.data ]));
@@ -139,25 +136,24 @@ async function handleDownload() {
 }
 
 function handleCreate() {
-	createOpen.value = true;
+  createOpen.value = true;
 }
 
-function showLabel(row: any)
-{
+function showLabel(row: any) {
   isShowLabel.value = true;
   rowData.value = row;
 }
 
 async function handleQuery() {
-	btnLoading.value = true;
-	await fetchList();
-	btnLoading.value = false;
+  btnLoading.value = true;
+  await fetchList();
+  btnLoading.value = false;
 }
 
 function resetParameter() {
-	for (const key in formInline) {
-		formInline[key] = '';
-	}
+  for (const key in formInline) {
+    formInline[key] = '';
+  }
 }
 
 defineExpose({ fetchList });
@@ -165,53 +161,53 @@ defineExpose({ fetchList });
 </script>
 
 <template>
-	<!--查询条件-->
-	<div ref="queryContainer" class="flex justify-between">
-		<div class="flex flex-1">
-			<div class="w-full whitespace-nowrap">
-				<el-row :gutter="20" style="margin-bottom: 5px;">
-					<el-col :span="6">
-						<div class="flex items-center">
-							<span class="mr-2">国 家</span>
-							<el-select v-model="formInline.country" clearable placeholder="请选择国家">
-								<el-option v-for="item in staticData.country_code" :key="item.value" :label="item.label"
-													 :value="item.value" />
-							</el-select>
-						</div>
-					</el-col>
-					<el-col :span="6">
-						<div class="flex items-center">
-							<span class="mr-2">用户评分</span>
-							<el-select v-model="formInline.score" clearable placeholder="请选择用户评分">
-								<el-option v-for="item in useScoreEnum" :key="item.value" :label="item.label"
-													 :value="item.value" />
-							</el-select>
-						</div>
-					</el-col>
-					<el-col :span="6">
-						<div class="flex items-center">
-							<span class="mr-2">评论类型</span>
-							<el-select v-model="formInline.tiv" clearable placeholder="请选择评论类型">
-								<el-option v-for="item in useTivEnum" :key="item.value" :label="item.label"
-													 :value="item.value" />
-							</el-select>
-						</div>
-					</el-col>
-				</el-row>
-			</div>
-		</div>
-		<VerticalDivider />
-		<div class="flex gap-1.5 ml-5">
-			<el-button :icon="Search" :loading="btnLoading" type="primary" @click="handleQuery">
-				查 询
-			</el-button>
-			<el-button :icon="RefreshRight" color="#ECECF1C9" style="width: 88px; color: #3c3c3c;" @click="resetParameter">
-				重 置
-			</el-button>
-		</div>
-	</div>
-	<el-divider ref="dividerContainer" style="margin: 20px 0 12px 0;" />
-  <vxe-grid ref="gridRef" v-bind="gridOptions">
+  <!--查询条件-->
+  <div ref="queryContainer" class="flex justify-between">
+    <div class="flex flex-1">
+      <div class="w-full whitespace-nowrap">
+        <el-row :gutter="20" style="margin-bottom: 5px;">
+          <el-col :span="6">
+            <div class="flex items-center">
+              <span class="mr-2">国 家</span>
+              <el-select v-model="formInline.country" clearable placeholder="请选择国家">
+                <el-option v-for="item in staticData.country_code" :key="item.value" :label="item.label"
+                           :value="item.value" />
+              </el-select>
+            </div>
+          </el-col>
+          <el-col :span="6">
+            <div class="flex items-center">
+              <span class="mr-2">用户评分</span>
+              <el-select v-model="formInline.score" clearable placeholder="请选择用户评分">
+                <el-option v-for="item in useScoreEnum" :key="item.value" :label="item.label"
+                           :value="item.value" />
+              </el-select>
+            </div>
+          </el-col>
+          <el-col :span="6">
+            <div class="flex items-center">
+              <span class="mr-2">评论类型</span>
+              <el-select v-model="formInline.tiv" clearable placeholder="请选择评论类型">
+                <el-option v-for="item in useTivEnum" :key="item.value" :label="item.label"
+                           :value="item.value" />
+              </el-select>
+            </div>
+          </el-col>
+        </el-row>
+      </div>
+    </div>
+    <VerticalDivider />
+    <div class="flex gap-1.5 ml-5">
+      <el-button :icon="Search" :loading="btnLoading" type="primary" @click="handleQuery">
+        查 询
+      </el-button>
+      <el-button :icon="RefreshRight" color="#ECECF1C9" style="width: 88px; color: #3c3c3c;" @click="resetParameter">
+        重 置
+      </el-button>
+    </div>
+  </div>
+  <el-divider ref="dividerContainer" style="margin: 20px 0 12px 0;" />
+  <vxe-grid ref="gridRef" class="z-0" v-bind="gridOptions">
     <template #toolbar_buttons>
       <div class="flex gap-2">
         <PermissionButton :icon="Plus" plain round type="primary" @click="handleCreate">
@@ -262,11 +258,11 @@ defineExpose({ fetchList });
     </template>
     <!-- 自定义列插槽 -->
     <template v-for="col in CompetitorMonitorCommentColumns" #[`${col.field}`]="{ row }">
-      <DataTableSlot :key="row.id" :field="col.field" :row="row" @open-negativeLabel="showLabel"/>
+      <DataTableSlot :key="row.id" :field="col.field" :row="row" @open-negativeLabel="showLabel" />
     </template>
   </vxe-grid>
-	<CreateDialog v-if="createOpen" v-model="createOpen" @refresh="fetchList" />
-	<NegativeLabel v-if="isShowLabel" v-model="isShowLabel" :rowData="rowData"></NegativeLabel>
+  <CreateDialog v-if="createOpen" v-model="createOpen" @refresh="fetchList" />
+  <NegativeLabel v-if="isShowLabel" v-model="isShowLabel" :rowData="rowData" />
 </template>
 
 <style scoped>

+ 56 - 58
src/views/product-manage/comment-detail/component/DataTableSlot.vue

@@ -6,11 +6,8 @@
  */
 
 import { useCountryInfoStore } from '/@/stores/countryInfo';
-import { Delete, InfoFilled, Operation, Tickets, Timer } from '@element-plus/icons-vue';
 import { getTagType } from '/@/utils/useTagColor';
 import PermissionButton from '/@/components/PermissionButton/index.vue';
-import ProductInfo from '/@/views/product-manage/component/ProductInfo.vue';
-import ProgressBar from '/@/views/product-manage/product-monitor/component/ProgressBar.vue';
 import { useTivEnum } from '/@/views/product-manage/comment-detail/enum';
 
 
@@ -20,30 +17,30 @@ const props = defineProps<{
 }>();
 const { row, field } = props;
 
-const emit: any = defineEmits([ 'open-negativeLabel']);
+const emit: any = defineEmits([ 'open-negativeLabel' ]);
 
 const countryInfoStore = useCountryInfoStore();
 const country = countryInfoStore.Countries.find(c => c.code == row.country_code);
 const color = country ? country.color : '#3875F6';
 
-function getCommentType(value): string {
-	const type = useTivEnum.find(item => item.value === value);
-	return type ? type.label : '未知';
+function getCommentType(value: any): string {
+  const type = useTivEnum.find(item => item.value === value);
+  return type ? type.label : '未知';
 }
 
 function getTagType(type: number): string {
-	switch (type) {
-		case 0:
-			return 'success';
-		case 1:
-			return 'primary';
-		case 2:
-			return 'warning';
-		case 3:
-			return 'danger';
-		default:
-			return 'info';
-	}
+  switch (type) {
+    case 0:
+      return 'success';
+    case 1:
+      return 'primary';
+    case 2:
+      return 'warning';
+    case 3:
+      return 'danger';
+    default:
+      return 'info';
+  }
 }
 
 function handleNegativeLabel() {
@@ -59,17 +56,18 @@ function handleNegativeLabel() {
         {{ country ? country.name : '-' }}
       </el-tag>
     </div>
-		<div v-else-if="field === 'title'">
-			<span style="font-weight: bold;">{{ row.title }}</span>
-		</div>
-		<div v-else-if="field === 'reviewer_name'">
-				<span style="font-weight: bold;">{{ row.reviewer_name }}</span>
-		</div>
-		<div v-else-if="field === 'content'">
-			<el-tooltip popper-class="custom-tooltip" effect="dark" :content="row.content" placement="top" :disabled="row.content.length <= 50">
-				<span class="content-ellipsis">{{ row.content }}</span>
-			</el-tooltip>
-		</div>
+    <div v-else-if="field === 'title'">
+      <span style="font-weight: bold;">{{ row.title }}</span>
+    </div>
+    <div v-else-if="field === 'reviewer_name'">
+      <span style="font-weight: bold;">{{ row.reviewer_name }}</span>
+    </div>
+    <div v-else-if="field === 'content'">
+      <el-tooltip :content="row.content" :disabled="row.content.length <= 50" :show-after="350" effect="dark" placement="top"
+                  popper-class="custom-tooltip">
+        <span class="content-ellipsis">{{ row.content }}</span>
+      </el-tooltip>
+    </div>
     <div v-else-if="field === 'score'">
       <template v-if="row.score !== null && row.score !== undefined && row.score !== ''">
         <el-rate
@@ -85,25 +83,25 @@ function handleNegativeLabel() {
         <span>-</span>
       </template>
     </div>
-		<div v-else-if="field === 'tiv'">
-			<el-tag :type="getTagType(row.tiv)">
-				{{ getCommentType(row.tiv) }}
-			</el-tag>
-		</div>
-		<div v-else-if="field === 'helpful'">
-				<span >{{ row.helpful }}</span>
-		</div>
-		<div v-else-if="field === 'verified'">
-			<el-switch v-model="row.verified" disabled></el-switch>
-		</div>
-		<div v-else-if="field === 'create_datetime'">
-				<span >{{ row.create_datetime }}</span>
-		</div>
+    <div v-else-if="field === 'tiv'">
+      <el-tag :type="getTagType(row.tiv)">
+        {{ getCommentType(row.tiv) }}
+      </el-tag>
+    </div>
+    <div v-else-if="field === 'helpful'">
+      <span>{{ row.helpful }}</span>
+    </div>
+    <div v-else-if="field === 'verified'">
+      <el-switch v-model="row.verified" disabled></el-switch>
+    </div>
+    <div v-else-if="field === 'create_datetime'">
+      <span>{{ row.create_datetime }}</span>
+    </div>
     <div v-else-if="field === 'operate'">
-      <div class="flex justify-center gap-2 mb-2">
-          <PermissionButton size="small" type="success" @click="handleNegativeLabel">
-						负面标签
-          </PermissionButton>
+      <div class="flex justify-center">
+        <PermissionButton size="small" type="danger" @click="handleNegativeLabel">
+          负面标签
+        </PermissionButton>
       </div>
     </div>
     <div v-else>
@@ -127,18 +125,18 @@ function handleNegativeLabel() {
 }
 
 .content-ellipsis {
-	display: -webkit-box;
-	-webkit-box-orient: vertical;
-	-webkit-line-clamp: 3; /* 显示的行数 */
-	overflow: hidden;
-	text-overflow: ellipsis;
-	max-height: 4.5em; /* 根据字号和行高设置最大高度 */
-	line-height: 1.5em; /* 控制行高,与你的内容相匹配 */
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 3; /* 显示的行数 */
+  overflow: hidden;
+  text-overflow: ellipsis;
+  max-height: 4.5em; /* 根据字号和行高设置最大高度 */
+  line-height: 1.5em; /* 控制行高,与你的内容相匹配 */
 }
 
 .custom-tooltip {
-	max-width: 350px;  /* 设置最大宽度 */
-	white-space: pre-wrap; /* 允许文本换行 */
-	word-wrap: break-word; /* 处理长单词换行 */
+  max-width: 350px; /* 设置最大宽度 */
+  white-space: pre-wrap; /* 允许文本换行 */
+  word-wrap: break-word; /* 处理长单词换行 */
 }
 </style>

+ 152 - 283
src/views/product-manage/comment-detail/component/NegativeLabel.vue

@@ -15,13 +15,18 @@ import EditLabelDialog from '/@/views/product-manage/comment-detail/component/Ed
 import { useResponse } from '/@/utils/useResponse';
 import { ElMessage } from 'element-plus';
 import { handleCopy } from '/@/utils/useCopyText';
+import * as echarts from 'echarts';
+import 'echarts-wordcloud';
+
 
 const isShowLabel = defineModel({ default: false });
 
+
 const props = defineProps({
-	rowData: <any>Object,
+  rowData: <any>Object,
 });
 
+
 const { rowData } = props;
 const { tableOptions, handlePageChange } = usePagination(fetchList);
 
@@ -32,317 +37,181 @@ const editData = ref<any>();
 
 const gridRef = ref();
 const gridOptions: any = reactive({
-	// id: 'NegativeLabel-table',
-	// keepSource: true,
-	height: 620,
-	size: 'mini',
-	border: false,
-	round: true,
-	stripe: true,
-	showHeader: true,
-	showOverflow: true,
-	currentRowHighLight: true,
-	toolbarConfig: {
-		size: 'large',
-		slots: {
-			buttons: 'toolbar_buttons',
-			// tools: 'toolbar_tools',
-		},
-	},
-	rowConfig: {
-		isHover: true,
-	},
-	columnConfig: {
-		resizable: true,
-	},
-	pagerConfig: {
-		total: tableOptions.value.total,
-		page: tableOptions.value.page,
-		limit: tableOptions.value.limit,
-	},
-	loading: false,
-	loadingConfig: {
-		icon: 'vxe-icon-indicator roll',
-		text: '正在拼命加载中...',
-	},
-	columns: '',
-	data: '',
+  // id: 'NegativeLabel-table',
+  // keepSource: true,
+  height: 620,
+  size: 'mini',
+  border: false,
+  round: true,
+  stripe: true,
+  showHeader: true,
+  showOverflow: true,
+  currentRowHighLight: true,
+  toolbarConfig: {
+    size: 'large',
+    slots: {
+      buttons: 'toolbar_buttons'
+      // tools: 'toolbar_tools',
+    }
+  },
+  rowConfig: {
+    isHover: true
+  },
+  columnConfig: {
+    resizable: true
+  },
+  pagerConfig: {
+    total: tableOptions.value.total,
+    page: tableOptions.value.page,
+    limit: tableOptions.value.limit
+  },
+  loading: false,
+  loadingConfig: {
+    icon: 'vxe-icon-indicator roll',
+    text: '正在拼命加载中...'
+  },
+  columns: '',
+  data: ''
 });
 
 async function fetchList() {
-	gridOptions.data = [];
-	gridOptions.columns = [];
-
-	const query = {
-		review: rowData.id,
-	};
-	await useTableData(api.getNegativeTags, query, gridOptions);
-	await gridRef.value.loadColumn(NegativeLabelColumns);
-	gridOptions.showHeader = Boolean(gridOptions.data?.length);
-}
-
-function handleCreate() {
-	createOpen.value = true;
-}
-
-function handleEdit(row) {
-	editOpen.value = true;
-	editData.value = row;
-}
-import { onBeforeUnmount, ref } from 'vue';
-import * as api from '../api';
-import * as echarts from 'echarts';
-import 'echarts-wordcloud';
-
-
-const props = defineProps({
-  asin: String
-});
-const { asin } = props;
-
-const loading = ref(false);
-
-let responseData: any = null;
-const chartRef: any = useTemplateRef('chartRef');
-let chart: echarts.ECharts | null = null;
-let resizeObserver: ResizeObserver | null = null;
-const hasData = ref(true);
-
-onBeforeMount(() => {
-  fetchWordCloudData();
-});
+  gridOptions.data = [];
+  gridOptions.columns = [];
 
-onBeforeUnmount(() => {
-  // 清理 ResizeObserver
-  if (resizeObserver) {
-    resizeObserver.disconnect();
-  }
-  if (chart) {
-    chart.dispose();
-    chart = null;
-  }
-});
-
-async function fetchWordCloudData() {
-  loading.value = true;
   const query = {
-    asin,
-    agg_field: 'raw_label'
+    review: rowData.id
   };
-  try {
-    responseData = await api.getWordCloudData(query);
-    if (!responseData.data || responseData.data.length === 0) {
-      // 处理空数据的情况
-      hasData.value = false;
-      if (chart) {
-        chart.clear();
-      }
-      return;
-    }
-    hasData.value = true;
-    setOption();
-    // initChart(option);
-  } catch (error) {
-    console.error('==Error==', error);
-  } finally {
-    loading.value = false;
-  }
+  await useTableData(api.getNegativeTags, query, gridOptions);
+  await gridRef.value.loadColumn(NegativeLabelColumns);
+  gridOptions.showHeader = Boolean(gridOptions.data?.length);
 }
 
-function setOption() {
-  const option = {
-    title: {
-      text: '负面标签'
-    },
-    tooltip: {
-      show: true
-    },
-    series: [
-      {
-        type: 'wordCloud',
-        width: '90%',
-        height: '85%',
-        // 词云图的形状
-        shape: 'star', // 可以是 'circle', 'cardioid', 'diamond', 'triangle-forward', 'triangle', 'pentagon', 'star'
-        // 控制字体大小
-        sizeRange: [12, 60],
-        // 文本旋转角度
-        rotationRange: [0, 0],
-        rotationStep: 45,
-        gridSize: 8,
-        drawOutOfBound: false,
-        shrinkToFit: false,
-        layoutAnimation: true,
-        // 全局的文字样式
-        textStyle: {
-          fontFamily: 'PingFang',
-          fontWeight: 'bold',
-          // Color can be a callback function or a color string
-          color: function () {
-            // Random color
-            return 'rgb(' + [
-              Math.round(Math.random() * 160),
-              Math.round(Math.random() * 160),
-              Math.round(Math.random() * 160)
-            ].join(',') + ')'
-          }
-        },
-        // 鼠标hover时的样式
-        emphasis: {
-          focus: 'self',
-          textStyle: {
-            textShadowBlur: 10,
-            textShadowColor: '#333'
-          }
-        },
-        // left: '10%',
-        // top: '10%',
-        // right: '10%',
-        // bottom: '10%',
-        data: responseData.data,
-      }
-    ],
-    backgroundColor: '#fff'
-  };
-
-  if (!chart) {
-    chart = echarts.init(chartRef.value);
-  }
-  chart.setOption(option);
+function handleCreate() {
+  createOpen.value = true;
+}
 
-  // 添加 ResizeObserver 以处理图表大小变化
-  if (!resizeObserver) {
-    resizeObserver = new ResizeObserver(() => {
-      chart?.resize();
-    });
-    resizeObserver.observe(chartRef.value);
-  }
+function handleEdit(row: any) {
+  editOpen.value = true;
+  editData.value = row;
 }
 
 async function singleDelete(row: any) {
-	const res = await useResponse(api.deleteNegativeTags, row);
-	if (res.code === 2000) {
-		ElMessage.success({ message: '删除成功', plain: true });
-		await fetchList();
-	}
+  const res = await useResponse(api.deleteNegativeTags, row);
+  if (res.code === 2000) {
+    ElMessage.success({ message: '删除成功', plain: true });
+    await fetchList();
+  }
 }
 
 function cellStyle() {
-	return{
-		fontWeight: 500,
-	}
+  return {
+    fontWeight: 500
+  };
 }
 
 onMounted(() => {
-	fetchList();
+  fetchList();
 });
 </script>
 
 <template>
-	<div class="drawer-container">
-		<el-drawer
-			ref="editDrawer"
-			v-model="isShowLabel"
-			:show-close="false"
-			direction="rtl"
-			size="50%"
-			style="background-color: #f3f4fb"
-			title="负面标签"
-		>
-			<div class="sticky top-0" style="background-color: #f3f4fb; min-height: 20px; z-index: 2"></div>
-			<div class="px-5">
-				<el-card class="mb-3">
-					<el-descriptions :column="1" direction="vertical">
-						<el-descriptions-item>
-							<template #label>
-								<div class="title-item">标题</div>
-							</template>
-							<span class="cell-item">{{ rowData.title }}</span>
-						</el-descriptions-item>
-						<el-descriptions-item>
-							<template #label>
-								<div class="title-item">评论内容</div>
-							</template>
-							<span class="cell-item">{{ rowData.content }}</span>
-							<el-icon class="ml-2 cursor-pointer" @click="handleCopy(rowData.content)">
-								<DocumentCopy />
-							</el-icon>
-						</el-descriptions-item>
-					</el-descriptions>
-				</el-card>
-				<el-card>
-					<vxe-grid ref="gridRef" v-bind="gridOptions" :cell-style="cellStyle">
-						<template #toolbar_buttons>
-							<div class="flex gap-2">
-								<PermissionButton :icon="Plus" plain round type="primary" @click="handleCreate"> 新 增 </PermissionButton>
-							</div>
-						</template>
-						<template #operate="{ row }">
-							<div class="flex justify-center gap-2">
-							<el-button :icon="Edit" link type="primary" @click="handleEdit(row)"></el-button>
-							<el-popconfirm
-								:icon="InfoFilled"
-								icon-color="#626AEF"
-								title="你确定要删除此项吗?"
-								width="220"
-								@confirm="singleDelete(row)"
-							>
-								<template #reference>
-									<PermissionButton link type="danger">
-										<el-icon>
-											<Delete />
-										</el-icon>
-									</PermissionButton>
-								</template>
-								<template #actions="{ confirm, cancel }">
-									<el-button size="small" @click="cancel">No!</el-button>
-									<el-button
-										size="small"
-										type="danger"
-										@click="confirm"
-									>
-										Yes?
-									</el-button>
-								</template>
-							</el-popconfirm>
-								</div>
-						</template>
-						<template #pager>
-							<vxe-pager
-								v-model:currentPage="gridOptions.pagerConfig.page"
-								v-model:pageSize="gridOptions.pagerConfig.limit"
-								:total="gridOptions.pagerConfig.total"
-								class="mt-1.5"
-								@page-change="handlePageChange"
-							/>
-						</template>
-					</vxe-grid>
-				</el-card>
-			</div>
-		</el-drawer>
-		<CreateLabelDialog v-if="createOpen" v-model="createOpen" :rowData="rowData" @refresh="fetchList"></CreateLabelDialog>
-		<EditLabelDialog v-if="editOpen" v-model="editOpen" :editData="editData" :rowData="rowData" @refresh="fetchList"></EditLabelDialog>
-	</div>
-  <el-card v-loading="loading" class="border-none" shadow="never">
-    <!-- 空状态 和 词云图 -->
-    <div class="w-full" style="min-height: 500px">
-      <div v-show="!loading && !hasData" style="min-height: 500px">
-        <el-empty :image-size="300" />
+  <div class="drawer-container">
+    <el-drawer
+        ref="editDrawer"
+        v-model="isShowLabel"
+        :show-close="false"
+        direction="rtl"
+        size="50%"
+        style="background-color: #f3f4fb"
+        title="负面标签"
+    >
+      <div class="sticky top-0" style="background-color: #f3f4fb; min-height: 20px; z-index: 2"></div>
+      <div class="px-5">
+        <el-card class="mb-3">
+          <el-descriptions :column="1" direction="vertical">
+            <el-descriptions-item>
+              <template #label>
+                <div class="title-item">标题</div>
+              </template>
+              <span class="cell-item">{{ rowData.title }}</span>
+            </el-descriptions-item>
+            <el-descriptions-item>
+              <template #label>
+                <div class="title-item">评论内容</div>
+              </template>
+              <span class="cell-item">{{ rowData.content }}</span>
+              <el-icon class="ml-2 cursor-pointer" @click="handleCopy(rowData.content)">
+                <DocumentCopy />
+              </el-icon>
+            </el-descriptions-item>
+          </el-descriptions>
+        </el-card>
+        <el-card>
+          <vxe-grid ref="gridRef" :cell-style="cellStyle" v-bind="gridOptions">
+            <template #toolbar_buttons>
+              <div class="flex gap-2">
+                <PermissionButton :icon="Plus" plain round type="primary" @click="handleCreate"> 新 增</PermissionButton>
+              </div>
+            </template>
+            <template #operate="{ row }">
+              <div class="flex justify-center gap-2">
+                <el-button :icon="Edit" link type="primary" @click="handleEdit(row)"></el-button>
+                <el-popconfirm
+                    :icon="InfoFilled"
+                    icon-color="#626AEF"
+                    title="你确定要删除此项吗?"
+                    width="220"
+                    @confirm="singleDelete(row)"
+                >
+                  <template #reference>
+                    <PermissionButton link type="danger">
+                      <el-icon>
+                        <Delete />
+                      </el-icon>
+                    </PermissionButton>
+                  </template>
+                  <template #actions="{ confirm, cancel }">
+                    <el-button size="small" @click="cancel">No!</el-button>
+                    <el-button
+                        size="small"
+                        type="danger"
+                        @click="confirm"
+                    >
+                      Yes?
+                    </el-button>
+                  </template>
+                </el-popconfirm>
+              </div>
+            </template>
+            <template #pager>
+              <vxe-pager
+                  v-model:currentPage="gridOptions.pagerConfig.page"
+                  v-model:pageSize="gridOptions.pagerConfig.limit"
+                  :total="gridOptions.pagerConfig.total"
+                  class="mt-1.5"
+                  @page-change="handlePageChange"
+              />
+            </template>
+          </vxe-grid>
+        </el-card>
       </div>
-      <div v-show="hasData" ref="chartRef" style="width: 100%; height: 500px"></div>
-    </div>
-  </el-card>
+    </el-drawer>
+    <CreateLabelDialog v-if="createOpen" v-model="createOpen" :rowData="rowData" @refresh="fetchList"></CreateLabelDialog>
+    <EditLabelDialog v-if="editOpen" v-model="editOpen" :editData="editData" :rowData="rowData"
+                     @refresh="fetchList"></EditLabelDialog>
+  </div>
+  
 </template>
 
 <style scoped>
 .title-item {
-	font-weight: bold;
-	font-size: 16px;
+  font-weight: bold;
+  font-size: 16px;
 }
 
 .cell-item {
-	font-size: 14px;
-	font-weight: 600;
-	color: #666;
+  font-size: 14px;
+  font-weight: 600;
+  color: #666;
 }
 </style>

+ 152 - 0
src/views/product-manage/comment-detail/component/NegativeLabelChart.vue

@@ -0,0 +1,152 @@
+<script setup lang="ts">
+/**
+ * @Name: NegativeLabelChart.vue
+ * @Description:
+ * @Author: Cheney
+ */
+import * as echarts from 'echarts';
+import * as api from '/@/views/product-manage/comment-detail/api';
+
+
+const props = defineProps({
+  asin: String
+});
+
+
+const { asin } = props;
+
+const loading = ref(false);
+
+let responseData: any = null;
+const chartRef: any = useTemplateRef('chartRef');
+let chart: echarts.ECharts | null = null;
+let resizeObserver: ResizeObserver | null = null;
+const hasData = ref(true);
+
+onBeforeMount(() => {
+  fetchWordCloudData();
+});
+
+onBeforeUnmount(() => {
+  // 清理 ResizeObserver
+  if (resizeObserver) {
+    resizeObserver.disconnect();
+  }
+  if (chart) {
+    chart.dispose();
+    chart = null;
+  }
+});
+
+async function fetchWordCloudData() {
+  loading.value = true;
+  const query = {
+    asin,
+    agg_field: 'raw_label'
+  };
+  try {
+    responseData = await api.getWordCloudData(query);
+    if (!responseData.data || responseData.data.length === 0) {
+      // 处理空数据的情况
+      hasData.value = false;
+      if (chart) {
+        chart.clear();
+      }
+      return;
+    }
+    hasData.value = true;
+    setOption();
+    // initChart(option);
+  } catch (error) {
+    console.error('==Error==', error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+function setOption() {
+  const option = {
+    title: {
+      text: '负面标签'
+    },
+    tooltip: {
+      show: true
+    },
+    series: [
+      {
+        type: 'wordCloud',
+        width: '90%',
+        height: '85%',
+        // 词云图的形状
+        shape: 'star', // 可以是 'circle', 'cardioid', 'diamond', 'triangle-forward', 'triangle', 'pentagon', 'star'
+        // 控制字体大小
+        sizeRange: [ 12, 60 ],
+        // 文本旋转角度
+        rotationRange: [ 0, 0 ],
+        rotationStep: 45,
+        gridSize: 8,
+        drawOutOfBound: false,
+        shrinkToFit: false,
+        layoutAnimation: true,
+        // 全局的文字样式
+        textStyle: {
+          fontFamily: 'PingFang',
+          fontWeight: 'bold',
+          // Color can be a callback function or a color string
+          color: function() {
+            // Random color
+            return 'rgb(' + [
+              Math.round(Math.random() * 160),
+              Math.round(Math.random() * 160),
+              Math.round(Math.random() * 160)
+            ].join(',') + ')';
+          }
+        },
+        // 鼠标hover时的样式
+        emphasis: {
+          focus: 'self',
+          textStyle: {
+            textShadowBlur: 10,
+            textShadowColor: '#333'
+          }
+        },
+        // left: '10%',
+        // top: '10%',
+        // right: '10%',
+        // bottom: '10%',
+        data: responseData.data
+      }
+    ],
+    backgroundColor: '#fff'
+  };
+
+  if (!chart) {
+    chart = echarts.init(chartRef.value);
+  }
+  chart.setOption(option);
+
+  // 添加 ResizeObserver 以处理图表大小变化
+  if (!resizeObserver) {
+    resizeObserver = new ResizeObserver(() => {
+      chart?.resize();
+    });
+    resizeObserver.observe(chartRef.value);
+  }
+}
+</script>
+
+<template>
+  <el-card v-loading="loading" class="border-none" shadow="never">
+    <!-- 空状态 和 词云图 -->
+    <div class="w-full" style="min-height: 500px">
+      <div v-show="!loading && !hasData" style="min-height: 500px">
+        <el-empty :image-size="300" />
+      </div>
+      <div v-show="hasData" ref="chartRef" style="width: 100%; height: 500px"></div>
+    </div>
+  </el-card>
+</template>
+
+<style scoped>
+
+</style>

+ 3 - 2
src/views/product-manage/comment-detail/component/TitleCard.vue

@@ -37,9 +37,10 @@ const { img, url, title, asin } = props;
       <div class="flex flex-col justify-between">
         <el-link :href="url"
                  :underline="false"
-                 style="font-size: 18px;"
+                 style="font-size: 18px;justify-content: left !important;"
+                 :disabled="!title"
                  type="primary">
-          <span class="line-clamp-2 text-ellipsis whitespace-normal">{{ title || '--' }}</span>
+          <span class="line-clamp-2 text-ellipsis whitespace-normal" >{{ title || '--' }}</span>
         </el-link>
         <div class="flex items-center">
           <div class="font-semibold italic">{{ asin }}</div>

+ 11 - 16
src/views/product-manage/comment-detail/index.vue

@@ -5,17 +5,12 @@
  * @Author: Cheney
  */
 
-import { DocumentCopy, Picture as IconPicture, RefreshRight, Search } from '@element-plus/icons-vue';
-import NegativeLabel from '/@/views/product-manage/comment-detail/component/NegativeLabel.vue';
 import NegativeClassification from '/@/views/product-manage/comment-detail/component/NegativeClassification.vue';
 import AverageMonthly from '/@/views/product-manage/comment-detail/component/AverageMonthly.vue';
 import TitleCard from './component/TitleCard.vue';
-import NegativeLabel from '/@/views/product-manage/comment-detail/component/NegativeLabel.vue';
-import NegativeClassification from '/@/views/product-manage/comment-detail/component/NegativeClassification.vue';
-
-import VerticalDivider from '/@/components/VerticalDivider/index.vue';
-import { handleCopy } from '/@/utils/useCopyText';
 import DataTable from '/@/views/product-manage/comment-detail/component/DataTable.vue';
+import NegativeLabelChart from '/@/views/product-manage/comment-detail/component/NegativeLabelChart.vue';
+
 
 const isShowComment = defineModel({ default: false });
 
@@ -42,25 +37,25 @@ const { rowData } = props;
       <div class="px-5">
         <!-- Title Card -->
         <TitleCard :asin="rowData.asin" :img="rowData.img" :title="rowData.title" :url="rowData.url" />
-         <!--Chart -->
+        <!--Chart -->
         <el-row :gutter="20" class="mt-5" style="z-index: 1">
           <el-col :span="8">
-            <NegativeLabel :asin="rowData.asin" />
+            <NegativeLabelChart :asin="rowData.asin" />
           </el-col>
           <el-col :span="16">
             <NegativeClassification :asin="rowData.asin" />
           </el-col>
         </el-row>
-         <!--Chart -->
+        <!--Chart -->
         <div class="mt-5">
           <AverageMonthly :asin="rowData.asin" />
         </div>
-				<!-- Data Table -->
-				<div class="mt-5">
-				<el-card class="border-none" shadow="hover">
-						<DataTable :asin="rowData.asin" />
-				</el-card>
-				</div>
+        <!-- Data Table -->
+        <div class="mt-5">
+          <el-card class="border-none" shadow="hover">
+            <DataTable :asin="rowData.asin" />
+          </el-card>
+        </div>
       </div>
     </el-drawer>