将行业热门话题洞察的需要修改成columns

This commit is contained in:
lq
2025-06-21 16:57:01 +08:00
29 changed files with 1312 additions and 150 deletions

7
package-lock.json generated
View File

@ -22,6 +22,7 @@
"sass": "^1.89.2", "sass": "^1.89.2",
"swiper": "^11.2.8", "swiper": "^11.2.8",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-cropper": "^1.1.4",
"vue-echarts": "^7.0.3", "vue-echarts": "^7.0.3",
"vue-router": "^4.1.6" "vue-router": "^4.1.6"
}, },
@ -8945,6 +8946,12 @@
"@vue/shared": "3.2.47" "@vue/shared": "3.2.47"
} }
}, },
"node_modules/vue-cropper": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/vue-cropper/-/vue-cropper-1.1.4.tgz",
"integrity": "sha512-5m98vBsCEI9rbS4JxELxXidtAui3qNyTHLHg67Qbn7g8cg+E6LcnC+hh3SM/p94x6mFh6KRxT1ttnta+wCYqWA==",
"license": "ISC"
},
"node_modules/vue-demi": { "node_modules/vue-demi": {
"version": "0.13.11", "version": "0.13.11",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz", "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz",

View File

@ -24,11 +24,10 @@
"mitt": "^3.0.0", "mitt": "^3.0.0",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pinia": "^2.0.29", "pinia": "^2.0.29",
"sass": "^1.89.2", "sass": "^1.89.2",
"swiper": "^11.2.8", "swiper": "^11.2.8",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-cropper": "^1.1.4",
"vue-echarts": "^7.0.3", "vue-echarts": "^7.0.3",
"vue-router": "^4.1.6" "vue-router": "^4.1.6"
}, },

View File

@ -1,5 +1,4 @@
import Http from '@/api'; import Http from '@/api';
// 导出一个函数,用于获取行业树 // 导出一个函数,用于获取行业树
export const fetchIndustriesTree = (params = {}) => { export const fetchIndustriesTree = (params = {}) => {
// 发送GET请求获取行业树 // 发送GET请求获取行业树
@ -50,7 +49,6 @@ export const fetchNewKeywordDetail = (params: any) => {
// 使用Http.get方法发送GET请求获取行业话题列表 // 使用Http.get方法发送GET请求获取行业话题列表
return Http.get('/v1/industry-new-keywords/' + params, {}); return Http.get('/v1/industry-new-keywords/' + params, {});
}; };
fetchIndustryTopicDetail;
// 导出一个函数fetchUserPainPointsList用于获取用户痛点列表 // 导出一个函数fetchUserPainPointsList用于获取用户痛点列表
export const fetchUserPainPointsList = (params: any) => { export const fetchUserPainPointsList = (params: any) => {
@ -98,7 +96,7 @@ export const fetchGenderDistributionsList = (params: any) => {
// 导出一个函数,用于获取产品列表 // 导出一个函数,用于获取产品列表
export const fetchProductList = () => { export const fetchProductList = () => {
// 使用Http.get方法发送GET请求获取产品列表 // 使用Http.get方法发送GET请求获取产品列表
return Http.get('/v1/products/list', {}, { headers: { 'enterprise-id': 1 } }); return Http.get('/v1/products/list');
}; };
// 导出一个函数,用于获取成功案例列表 // 导出一个函数,用于获取成功案例列表
@ -109,5 +107,55 @@ export const fetchSuccessCaseList = () => {
// 试用产品 // 试用产品
export const trialProduct = (id: number) => { export const trialProduct = (id: number) => {
return Http.post(`/v1/products/${id}/try`, {}, { headers: { 'enterprise-id': 1 } }); return Http.post(`/v1/products/${id}/try`);
};
// 修改企业名称
export const updateEnterpriseName = (data: any) => {
return Http.patch(`/v1/enterprises/name`, data);
};
// 发送修改手机号验证码
export const sendUpdateMobileCaptcha = (data: any) => {
return Http.post(`/v1/sms/update-mobile-captcha`, data);
};
// 修改绑定的手机号
export const updateMobile = (data: any) => {
return Http.post(`/v1/me/mobile`, data);
};
// 修改我的信息
export const updateMyInfo = (data: any) => {
return Http.put(`/v1/me`, data);
};
// 获取企业账号分页
export const fetchSubAccountPage = (params: any) => {
return Http.get(`/v1/enterprises/users`, params);
};
// 获取企业账号分页
export const fetchImageUploadFile = (params: any) => {
return Http.get(`/v1/oss/image-pre-signed-url`, params);
};
// 移除企业子账号
export const removeEnterpriseAccount = (userId: number) => {
return Http.delete(`/v1/enterprises/users/${userId}`);
};
// 获取企业邀请码
export const getEnterpriseInviteCode = () => {
return Http.get(`/v1/enterprises/invite-code`);
};
// 根据邀请码获取企业信息
export const getEnterpriseByInviteCode = (inviteCode: string) => {
return Http.get(`/v1/enterprises/by-invite-code`, { invite_code: inviteCode });
};
// 根据邀请码加入企业
export const joinEnterpriseByInviteCode = (inviteCode: string) => {
return Http.post(`/v1/enterprises/join`, { invite_code: inviteCode });
}; };

View File

@ -21,6 +21,11 @@ const HttpStatusCode = {
InternalServerError: 500, InternalServerError: 500,
}; };
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import pinia from '@/stores';
const store = useEnterpriseStore(pinia);
const enterprise = store.getEnterpriseInfo();
//* 导出Request类可以用来自定义传递配置来创建实例 //* 导出Request类可以用来自定义传递配置来创建实例
export class Request { export class Request {
//* axios 实例 //* axios 实例
@ -41,6 +46,15 @@ export class Request {
(config: AxiosRequestConfig) => { (config: AxiosRequestConfig) => {
const token = localStorage.getItem('accessToken') as string; const token = localStorage.getItem('accessToken') as string;
config.headers!.Authorization = token; config.headers!.Authorization = token;
if (token) {
config.headers!.Authorization = token;
} else {
config.headers!.satoken = '123';
}
if (enterprise) {
config.headers!['enterprise-id'] = enterprise.id;
}
return config; return config;
}, },
(err: any) => { (err: any) => {
@ -87,6 +101,10 @@ export class Request {
public delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> { public delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.instance.delete(url, config); return this.instance.delete(url, config);
} }
public patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.instance.patch(url, data, config);
}
} }
//* 默认导出Request实例 //* 默认导出Request实例

3
src/assets/warning.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.66675 9.99984C1.66675 5.39746 5.39771 1.6665 10.0001 1.6665C14.6025 1.6665 18.3334 5.39746 18.3334 9.99984C18.3334 14.6022 14.6025 18.3332 10.0001 18.3332C5.39771 18.3332 1.66675 14.6022 1.66675 9.99984ZM9.16675 12.4998V14.1665H10.8334V12.4998H9.16675ZM10.8334 11.6665L10.8334 5.83317L9.16675 5.83317L9.16675 11.6665H10.8334Z" fill="#FFAE00"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

View File

@ -10,7 +10,9 @@ const clickExit = () => {
}; };
const getMenus = async () => { const getMenus = async () => {
const res = await fetchMenusTree(); const res = await fetchMenusTree();
lists.value = res; if (res.code == 200) {
lists.value = res.data;
}
}; };
onMounted(() => { onMounted(() => {
getMenus(); getMenus();

View File

@ -1,6 +1,9 @@
<template> <template>
<div class="container"> <div class="container">
<h1 class="title">{{ props.title }}</h1> <div class="flex item-center arco-row-justify-space-between">
<h1 class="title">{{ props.title }}</h1>
<slot name="header"></slot>
</div>
<div> <div>
<slot></slot> <slot></slot>
</div> </div>
@ -17,6 +20,7 @@ const props = defineProps<{
border: 1px solid var(--BG-300, rgba(230, 230, 232, 1)); border: 1px solid var(--BG-300, rgba(230, 230, 232, 1));
background: var(--BG-white, rgba(255, 255, 255, 1)); background: var(--BG-white, rgba(255, 255, 255, 1));
padding: 16px 24px 20px 24px; padding: 16px 24px 20px 24px;
border-radius: 8px;
} }
.title { .title {
font-family: Alibaba PuHuiTi, serif; font-family: Alibaba PuHuiTi, serif;
@ -24,6 +28,7 @@ const props = defineProps<{
font-size: 18px; font-size: 18px;
line-height: 24px; line-height: 24px;
vertical-align: middle; vertical-align: middle;
margin-bottom: 0px; margin: 0;
padding: 0;
} }
</style> </style>

View File

@ -0,0 +1,12 @@
<template>
<Modal title="扫描下面二维码联系客户" v-bind="$attrs">
<div class="text-center mt-16px mb-16px">
<img width="200" src="@/assets/customer-service.svg" alt="" />
</div>
</Modal>
</template>
<script setup lang="ts">
import Modal from '@/components/modal.vue';
</script>
<style lang="less">
</style>

View File

@ -0,0 +1,58 @@
<template>
<a-modal modal-class="delete-modal" body-class="body" cancel-text="返回" ok-text="确定删除" v-bind="$attrs">
<h2 class="delete-modal-title flex item-center">
<img src="@/assets/warning.svg" alt="" />
{{ $attrs.title }}
</h2>
<slot></slot>
</a-modal>
</template>
<script setup lang="ts">
</script>
<style lang="less">
:deep(.arco-btn-status-danger) {
background-color: red !important;
width: 1000px !important;
}
.delete-modal {
.arco-modal-header {
display: none;
}
.delete-modal-title {
margin-top: 24px;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
color: var(--Text-1, rgba(33, 31, 36, 1));
img {
width: 20px;
height: 20px;
margin-right: 12px;
}
}
.arco-modal-footer {
border-top: none;
:first-child {
border: 1px solid var(--BG-500, rgba(177, 178, 181, 1));
border-radius: 4px;
padding: 7px 20px;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
}
:last-child {
border-radius: 4px;
padding: 7px 20px;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
margin-left: 16px;
border-color: var(--Functional-Danger-6, rgba(246, 75, 49, 1)) !important;
background-color: var(--Functional-Danger-6, rgba(246, 75, 49, 1)) !important;
}
}
}
.body {
padding: 0 24px;
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<Modal title="加入企业" @ok="handleJoin">
<div v-if="enterprise" class="join-body flex item-center">
<img src="@/assets/warning.svg" alt="" />
{{ `确定加入 “${enterprise.name}”吗?` }}
</div>
</Modal>
</template>
<script setup lang="ts">
import Modal from '@components/modal.vue';
import { ref, onMounted } from 'vue';
import { getQueryParam } from '@/utils/helper';
import { getEnterpriseByInviteCode, joinEnterpriseByInviteCode } from '@/api/all';
const enterprise = ref();
const inviteCode = ref();
async function getEnterprise() {
inviteCode.value = getQueryParam('invite_code');
if (inviteCode.value) {
enterprise.value = await getEnterpriseByInviteCode(inviteCode.value);
}
}
async function handleJoin() {
await joinEnterpriseByInviteCode(inviteCode.value);
AMessage.success('加入成功');
}
onMounted(() => {
getEnterprise();
});
</script>
<style lang="less">
.join-body {
margin: 0;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
color: var(--Text-1, rgba(33, 31, 36, 1));
img {
width: 20px;
height: 20px;
margin-right: 12px;
margin-left: 0;
}
}
</style>

38
src/components/modal.vue Normal file
View File

@ -0,0 +1,38 @@
<template>
<a-modal title-align="start" modal-class="modal" body-class="body" v-bind="$attrs" >
<slot></slot>
</a-modal>
</template>
<script setup lang="ts">
</script>
<style lang="less">
.modal {
.arco-modal-header {
border-bottom: none;
}
.arco-modal-footer {
border-top: none;
:first-child {
border: 1px solid var(--BG-500, rgba(177, 178, 181, 1));
border-radius: 4px;
padding: 7px 20px;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
}
:last-child {
border-radius: 4px;
padding: 7px 20px;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
margin-left: 16px;
}
}
}
.body {
padding-top: 0;
padding-bottom: 0;
}
</style>

View File

@ -1,7 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAppStore } from '@/stores'; import { useAppStore } from '@/stores';
import { useResponsive } from '@/hooks'; import { useResponsive } from '@/hooks';
import JoinModal from '@/components/join-modal.vue';
import { getQueryParam } from '@/utils/helper';
import { ref, onMounted } from 'vue';
const joinEnterpriseVisible = ref(false);
const appStore = useAppStore(); const appStore = useAppStore();
const router = useRouter(); const router = useRouter();
@ -27,11 +31,19 @@ const route = useRoute();
// onMounted(() => { // onMounted(() => {
// showSidebar.value = route.meta.requiresSidebar == true; // showSidebar.value = route.meta.requiresSidebar == true;
// }); // });
onMounted(() => {
checkHasInviteCode();
});
const setCollapsed = (val: boolean) => { const setCollapsed = (val: boolean) => {
appStore.updateSettings({ menuCollapse: val }); appStore.updateSettings({ menuCollapse: val });
}; };
const checkHasInviteCode = () => {
const inviteCode = getQueryParam('invite_code');
if (inviteCode) {
joinEnterpriseVisible.value = true;
}
};
const drawerVisible = ref(false); const drawerVisible = ref(false);
const drawerCancel = () => { const drawerCancel = () => {
drawerVisible.value = false; drawerVisible.value = false;
@ -43,6 +55,7 @@ provide('toggleDrawerMenu', () => {
<template> <template>
<a-layout :class="['layout', { mobile: appStore.hideMenu }]"> <a-layout :class="['layout', { mobile: appStore.hideMenu }]">
<JoinModal v-model:visible="joinEnterpriseVisible" />
<div v-if="navbar" class="layout-navbar"> <div v-if="navbar" class="layout-navbar">
<base-navbar /> <base-navbar />
</div> </div>

View File

@ -50,6 +50,21 @@ const router = createRouter({
name: 'auth', name: 'auth',
component: () => import('@/views/components/permission/auth.vue'), component: () => import('@/views/components/permission/auth.vue'),
}, },
{
path: '/management/person',
name: 'person',
component: () => import('@/views/components/management/person'),
},
{
path: '/management/enterprise',
name: 'enterprise',
component: () => import('@/views/components/management/enterprise'),
},
{
path: '/management/account',
name: 'account',
component: () => import('@/views/components/management/account'),
},
], ],
scrollBehavior() { scrollBehavior() {
return { top: 0 }; return { top: 0 };

View File

@ -0,0 +1,49 @@
interface EnterpriseInfo {
id: number;
name: string;
update_name_quota: number;
used_update_name_count: number;
sub_account_quota: number;
used_sub_account_count: number;
}
interface EnterpriseState {
enterpriseInfo: EnterpriseInfo | null;
}
export const useEnterpriseStore = defineStore('enterprise', {
state: (): EnterpriseState => ({
// todo 暂时写死登录功能完成后记得重置为null哦
enterpriseInfo: {
id: 1,
name: '企业1',
update_name_quota: 2,
used_update_name_count: 1,
sub_account_quota: 2,
used_sub_account_count: 2,
},
}),
actions: {
setEnterpriseInfo(enterpriseInfo: EnterpriseInfo) {
this.enterpriseInfo = enterpriseInfo;
},
setEnterpriseName(name: string) {
if (this.enterpriseInfo) {
this.enterpriseInfo.name = name;
}
},
incUsedUpdateNameCount() {
if (this.enterpriseInfo) {
this.enterpriseInfo.used_update_name_count++;
}
},
incUsedSubAccountCount() {
if (this.enterpriseInfo) {
this.enterpriseInfo.used_sub_account_count++;
}
},
getEnterpriseInfo(): EnterpriseInfo | null {
return this.enterpriseInfo;
},
},
});

View File

@ -19,6 +19,13 @@ interface UserState {
token: string; token: string;
userInfo: UserInfo | null; userInfo: UserInfo | null;
companyInfo: CompanyInfo | null; companyInfo: CompanyInfo | null;
isLogin: boolean;
}
interface UserInfo {
id: number;
mobile: string;
name: string;
} }
export const useUserStore = defineStore('user', { export const useUserStore = defineStore('user', {
@ -26,67 +33,47 @@ export const useUserStore = defineStore('user', {
token: localStorage.getItem('accessToken') || '', token: localStorage.getItem('accessToken') || '',
userInfo: null, userInfo: null,
companyInfo: null, companyInfo: null,
isLogin: false,
}), }),
actions: { actions: {
setToken(token: String) { // 设置 Token
const _token = `Bearer ${token}`; setToken(token: string) {
this.token = _token; this.token = `Bearer ${token}`;
localStorage.setItem('accessToken', _token); localStorage.setItem('accessToken', this.token);
}, },
// 存储用户信息 // 获取 Token
getToken() {
return this.token;
},
// 设置用户信息
setUserInfo(userInfo: UserInfo | null) { setUserInfo(userInfo: UserInfo | null) {
this.userInfo = userInfo; this.userInfo = userInfo;
if (userInfo) {
localStorage.setItem('userInfo', JSON.stringify(userInfo));
} else {
localStorage.removeItem('userInfo');
}
}, },
// 获取用户信息 // 获取用户信息
getUserInfo(): UserInfo | null { getUserInfo(): UserInfo | null {
const userInfoStr = localStorage.getItem('userInfo'); return this.userInfo;
if (userInfoStr) {
try {
return JSON.parse(userInfoStr);
} catch (error) {
console.error('解析用户信息失败:', error);
return null;
}
}
return null;
}, },
// 存储公司信息 // 设置公司信息
setCompanyInfo(companyInfo: CompanyInfo | null) { setCompanyInfo(companyInfo: CompanyInfo | null) {
this.companyInfo = companyInfo; this.companyInfo = companyInfo;
if (companyInfo) {
localStorage.setItem('companyInfo', JSON.stringify(companyInfo));
} else {
localStorage.removeItem('companyInfo');
}
}, },
// 获取公司信息 // 获取公司信息
getCompanyInfo(): CompanyInfo | null { getCompanyInfo(): CompanyInfo | null {
const companyInfoStr = localStorage.getItem('companyInfo'); return this.companyInfo;
if (companyInfoStr) {
try {
return JSON.parse(companyInfoStr);
} catch (error) {
console.error('解析公司信息失败:', error);
return null;
}
}
return null;
}, },
// 删除 token // 登录状态
deleteToken() { setIsLogin(isLogin: boolean) {
this.token = ''; this.isLogin = isLogin;
localStorage.removeItem('accessToken');
}, },
},
getIsLogin(): boolean {
return this.isLogin;
}
}
}); });

5
src/utils/helper.ts Normal file
View File

@ -0,0 +1,5 @@
export function getQueryParam(param: string): string | null {
const search = window.location.search || window.location.hash.split('?')[1] || '';
const params = new URLSearchParams(search);
return params.get(param);
}

View File

@ -11,7 +11,7 @@
</template> </template>
</a-button> </a-button>
<template #content> <template #content>
<p>基于xxx获取数据xxx一段文字描述该数据的获取方式和来源等xxx</p> <p style="margin: 0">基于行业内内容提取的高频词汇</p>
</template> </template>
</a-popover> </a-popover>
</a-space> </a-space>
@ -84,12 +84,17 @@ const getIndustryTerms = async () => {
industry_id: selectedIndustry.value, industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value, time_dimension: selectedTimePeriod.value,
}; };
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value !== 0) { if (selectedSubCategory.value !== 0) {
params['industry_id'] = selectedSubCategory.value; params['industry_id'] = selectedSubCategory.value;
} }
const res = await fetchindustryTerms(params); const res = await fetchindustryTerms(params);
// 这里需要根据API返回的数据结构处理成tagRows需要的格式 if (res.code === 200) {
tagRows.value = processTagData(res.slice(0, 70)); // 这里需要根据API返回的数据结构处理成tagRows需要的格式
tagRows.value = processTagData(res.data.slice(0, 70));
}
}; };
// 标签数据(按行分组) // 标签数据(按行分组)

View File

@ -12,62 +12,46 @@
</template> </template>
</a-button> </a-button>
<template #content> <template #content>
<p>基于xxx获取数据xxx一段文字描述该数据的获取方式和来源等xxx</p> <p style="margin: 0">基于社交内容平台的行业数据分析用户关注的热门话题与趋势</p>
</template> </template>
</a-popover> </a-popover>
</a-space> </a-space>
<a-table :data="dataList"> <a-table :columns="columns" :data="dataList" :filter-icon-align-left="alignLeft" @change="handleChange">
<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 }"> <template #rank="{ record }">
<a-tag color="blue" v-if="record.rank == 1">1</a-tag> <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>
<template #keywords="{ record }">
<a-tag v-for="item in record.keywords" :key="item" style="margin-right: 5px">{{ item }}</a-tag>
</template>
<template #hot="{ record }">
<img v-for="i in record.hot" :key="i" :src="starImages[i - 1]" style="width: 16px; height: 16px" />
</template>
<template #sentiment="{ 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>
<template #optional="{ record }">
<a-button type="outline" @click="gotoDetail(record)">详情</a-button>
</template> </template>
</a-table> </a-table>
</a-space> </a-space>
<!-- modal -->
<a-modal :visible="visible" @ok="handleOk" @cancel="handleCancel" unmountOnClose> <a-modal :visible="visible" @ok="handleOk" @cancel="handleCancel" unmountOnClose>
<template #title> <template #title>
<span style="text-align: left; width: 100%">行业热门话题洞察</span> <span style="text-align: left; width: 100%">行业热门话题洞察</span>
@ -135,12 +119,63 @@ import star5 from '@/assets/img/hottranslation/star-fill5.png';
import top1 from '@/assets/img/captcha/top1.svg'; import top1 from '@/assets/img/captcha/top1.svg';
import top2 from '@/assets/img/captcha/top2.svg'; import top2 from '@/assets/img/captcha/top2.svg';
import top3 from '@/assets/img/captcha/top3.svg'; import top3 from '@/assets/img/captcha/top3.svg';
import { IconQuestionCircle, IconArrowUp, IconArrowDown } from '@arco-design/web-vue/es/icon';
// 新增排序状态和函数
const heatSortDirection = ref('desc'); // 默认降序排列
const columns = [
{
title: '排名',
dataIndex: 'rank',
slotName: 'rank',
},
{
title: '话题名称',
dataIndex: 'name',
},
{
title: '关键词',
dataIndex: 'keywords',
slotName: 'keywords',
},
{
title: '热度指数',
dataIndex: 'hot',
sortable: {
sortDirections: ['ascend', 'descend'],
},
slotName: 'hot',
},
{
title: '情感倾向',
dataIndex: 'sentiment',
slotName: 'sentiment',
},
{
title: '操作',
slotName: 'optional',
},
];
// 切换排序方向
const toggleHeatSort = () => {
heatSortDirection.value = heatSortDirection.value === 'asc' ? 'desc' : 'asc';
sortDataByHeat();
};
// 实际排序逻辑
const sortDataByHeat = () => {
dataList.value.sort((a, b) => {
return heatSortDirection.value === 'asc' ? a.hot - b.hot : b.hot - a.hot;
});
// 排序后更新排名
dataList.value.forEach((item, index) => {
item.rank = index + 1;
});
};
const starImages = [star1, star2, star3, star4, star5]; const starImages = [star1, star2, star3, star4, star5];
const topImages = [top1, top2, top3]; const topImages = [top1, top2, top3];
// 行业大类 // 行业大类
const industriesTree = ref([]); const industriesTree = ref([]);
// 行业热门话题洞察 // 行业热门话题洞察
const dataList = ref([]); const dataList = ref([]);
// 显示详情 // 显示详情
@ -174,26 +209,36 @@ const getIndustriesTree = async () => {
getIndustryTopics(); getIndustryTopics();
}; };
const handleSort = () => {
console.log('table change');
};
// 行业热门话题 // 行业热门话题
const getIndustryTopics = async () => { const getIndustryTopics = async () => {
let parms = { let parms = {
industry_id: selectedIndustry.value, industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value, time_dimension: selectedTimePeriod.value,
}; };
if (selectedSubCategory.value != 0) { if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value != undefined && selectedSubCategory.value != 0) {
parms['industry_id'] = selectedSubCategory.value; parms['industry_id'] = selectedSubCategory.value;
} }
const res = await fetchIndustryTopics(parms); const res = await fetchIndustryTopics(parms);
dataList.value = res; if (res.code == 200) {
dataList.value = res.data;
}
}; };
// 详情 // 详情
const gotoDetail = async (record) => { const gotoDetail = async (record) => {
console.log(record); console.log(record);
const res = await fetchIndustryTopicDetail(record.id); const res = await fetchIndustryTopicDetail(record.id);
console.log(res); if (res.code == 200) {
visible.value = true; visible.value = true;
topicInfo.value = res; topicInfo.value = res.data;
}
}; };
// 弹窗的取消 // 弹窗的取消
@ -230,4 +275,14 @@ const handleOk = () => {
color: #737478 !important; color: #737478 !important;
margin-left: -5px; margin-left: -5px;
} }
:deep(.arco-icon) {
display: inline-block;
vertical-align: middle;
}
/* 按钮悬停效果 */
:deep(.arco-btn-text:not(.arco-btn-disabled):hover) {
background-color: transparent;
}
</style> </style>

View File

@ -12,7 +12,7 @@
</template> </template>
</a-button> </a-button>
<template #content> <template #content>
<p>基于xxx获取数据xxx一段文字描述该数据的获取方式和来源等xxx</p> <p style="margin: 0">基于该行业中近期提及频次高用户互动活跃的品牌内容筛选出关注度较高的代表性品牌</p>
</template> </template>
</a-popover> </a-popover>
</a-space> </a-space>
@ -70,7 +70,9 @@
</template> </template>
</a-button> </a-button>
<template #content> <template #content>
<p>基于xxx获取数据xxx一段文字描述该数据的获取方式和来源等xxx</p> <p style="margin: 0">
基于情绪分析与敏感词识别对行业内容中的负面或争议性话题进行监测辅助判断舆情风险动态
</p>
</template> </template>
</a-popover> </a-popover>
</a-space> </a-space>
@ -116,6 +118,12 @@ const getFocusBrandsList = async () => {
industry_id: selectedIndustry.value, industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value, time_dimension: selectedTimePeriod.value,
}; };
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) { if (selectedSubCategory.value != 0) {
params['industry_id'] = selectedSubCategory.value; params['industry_id'] = selectedSubCategory.value;
} }
@ -129,6 +137,12 @@ const getEventDynamicsList = async () => {
industry_id: selectedIndustry.value, industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value, time_dimension: selectedTimePeriod.value,
}; };
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) { if (selectedSubCategory.value != 0) {
params['industry_id'] = selectedSubCategory.value; params['industry_id'] = selectedSubCategory.value;
} }

View File

@ -12,7 +12,7 @@
</template> </template>
</a-button> </a-button>
<template #content> <template #content>
<p>基于xxx获取数据xxx一段文字描述该数据的获取方式和来源等xxx</p> <p style="margin: 0">基于该行业用户内容中提及频率较高的关键词按热度进行排序反映近期关注焦点</p>
</template> </template>
</a-popover> </a-popover>
</a-space> </a-space>
@ -92,7 +92,9 @@
</template> </template>
</a-button> </a-button>
<template #content> <template #content>
<p>基于xxx获取数据xxx一段文字描述该数据的获取方式和来源等xxx</p> <p style="margin: 0">
对该行业下用户内容进行情绪分析按情绪类别统计占比提取占比最高者作为行业情绪代表
</p>
</template> </template>
</a-popover> </a-popover>
</a-space> </a-space>
@ -153,7 +155,7 @@
</template> </template>
</a-button> </a-button>
<template #content> <template #content>
<p>基于xxx获取数据xxx一段文字描述该数据的获取方式和来源等xxx</p> <p style="margin: 0">指当前周期中首次出现或相较上一周期词频显著增长的关键词反映近期出现的新关注点</p>
</template> </template>
</a-popover> </a-popover>
</a-space> </a-space>
@ -306,6 +308,12 @@ const getIndustryEmotions = async () => {
industry_id: selectedIndustry.value, industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value, time_dimension: selectedTimePeriod.value,
}; };
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) { if (selectedSubCategory.value != 0) {
params['industry_id'] = selectedSubCategory.value; params['industry_id'] = selectedSubCategory.value;
} }
@ -356,6 +364,12 @@ const getKeywordTrendsList = async () => {
industry_id: selectedIndustry.value, industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value, time_dimension: selectedTimePeriod.value,
}; };
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) { if (selectedSubCategory.value != 0) {
params['industry_id'] = selectedSubCategory.value; params['industry_id'] = selectedSubCategory.value;
} }
@ -380,12 +394,20 @@ const getNewKeywordList = async () => {
industry_id: selectedIndustry.value, industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value, time_dimension: selectedTimePeriod.value,
}; };
if (selectedSubCategory.value != 0) { if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0 && selectedSubCategory.value != undefined) {
params['industry_id'] = selectedSubCategory.value; params['industry_id'] = selectedSubCategory.value;
} }
const res = await fetchNewKeywordList(params); const res = await fetchNewKeywordList(params);
// 这里需要根据API返回的数据结构处理成tagRows需要的格式 if (res.code == 200) {
keywordList.value = res; // 这里需要根据API返回的数据结构处理成tagRows需要的格式
keywordList.value = res.data;
}
}; };
const drawChart = () => { const drawChart = () => {

View File

@ -5,10 +5,9 @@
direction="vertical" direction="vertical"
style="background-color: #fff; width: 100%; padding: 24px; margin: 24px 0; color: #737478; font-size: 14px" style="background-color: #fff; width: 100%; padding: 24px; margin: 24px 0; color: #737478; font-size: 14px"
> >
<a-space align="center"> <a-space align="start" style="width: 100%; margin-top: 20px; align-items: flex-start">
<!-- 行业选择 --> <span style="width: 60px; flex-shrink: 0; line-height: 28px">行业大类</span>
<a-space align="center"> <div style="display: flex; flex-wrap: wrap; gap: 8px; width: 100%; align-items: flex-start">
<span>行业大类</span>
<a-tag <a-tag
size="Medium" size="Medium"
v-for="item in industriesTree" v-for="item in industriesTree"
@ -24,12 +23,12 @@
" "
>{{ item.name }}</a-tag >{{ item.name }}</a-tag
> >
</a-space> </div>
</a-space> </a-space>
<a-space align="center" style="margin-left: 'auto'; margin-top: 20px"> <!-- 二级类目 -->
<!-- 二级类目 --> <a-space align="start" style="width: 100%; margin-top: 20px; align-items: flex-start">
<a-space align="center"> <span style="width: 60px; flex-shrink: 0; line-height: 28px">二级类目</span>
<span>二级类目</span> <div style="display: flex; flex-wrap: wrap; gap: 8px; width: 100%; align-items: flex-start">
<a-tag <a-tag
size="Medium" size="Medium"
v-for="item in subCategories" v-for="item in subCategories"
@ -45,12 +44,12 @@
" "
>{{ item.name }}</a-tag >{{ item.name }}</a-tag
> >
</a-space> </div>
</a-space> </a-space>
<a-space align="center" style="margin-left: 'auto'; margin-top: 20px"> <!-- </a-space> -->
<!-- 时间筛选 --> <a-space align="start" style="width: 100%; margin-top: 20px; align-items: flex-start">
<a-space align="center"> <span style="width: 60px; flex-shrink: 0; line-height: 28px">时间筛选</span>
<span>时间筛选</span> <div style="display: flex; flex-wrap: wrap; gap: 8px; width: 100%; align-items: flex-start">
<a-tag <a-tag
size="Medium" size="Medium"
v-for="item in timePeriods" v-for="item in timePeriods"
@ -66,7 +65,7 @@
" "
>{{ item.label }} >{{ item.label }}
</a-tag> </a-tag>
</a-space> </div>
</a-space> </a-space>
<!-- 搜索区域 --> <!-- 搜索区域 -->
<a-space style="margin-left: 'auto'; margin-top: 20px"> <a-space style="margin-left: 'auto'; margin-top: 20px">
@ -161,11 +160,14 @@ onMounted(() => {
// 获取行业大类数据 // 获取行业大类数据
const getIndustriesTree = async () => { const getIndustriesTree = async () => {
const res = await fetchIndustriesTree(); const res = await fetchIndustriesTree();
industriesTree.value = res; if (res.code == 200) {
selectedIndustry.value = res[0].id; let data = res['data'];
selectedSubCategory.value = 0; industriesTree.value = data;
subCategories.value = [...industriesTree.value[0].children]; selectedIndustry.value = data[0].id;
subCategories.value.unshift({ id: 0, name: '全部' }); selectedSubCategory.value = 0;
subCategories.value = [...industriesTree.value[0].children];
subCategories.value.unshift({ id: 0, name: '全部' });
}
}; };
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'search'): void; (e: 'search'): void;

View File

@ -13,7 +13,7 @@
</template> </template>
</a-button> </a-button>
<template #content> <template #content>
<p>基于xxx获取数据xxx一段文字描述该数据的获取方式和来源等xxx</p> <p style="margin: 0">基于用户内容中的情绪分析与表达模式提取反复出现的负面倾向主题反映典型使用痛点</p>
</template> </template>
</a-popover> </a-popover>
</a-space> </a-space>
@ -140,6 +140,12 @@ const getUserPainPointsList = async () => {
industry_id: selectedIndustry.value, industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value, time_dimension: selectedTimePeriod.value,
}; };
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) { if (selectedSubCategory.value != 0) {
params['industry_id'] = selectedSubCategory.value; params['industry_id'] = selectedSubCategory.value;
} }

View File

@ -173,6 +173,12 @@ const getAgeDistributionsList = async () => {
industry_id: selectedIndustry.value, industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value, time_dimension: selectedTimePeriod.value,
}; };
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) { if (selectedSubCategory.value != 0) {
parms['industry_id'] = selectedSubCategory.value; parms['industry_id'] = selectedSubCategory.value;
} }
@ -189,6 +195,12 @@ const getGeoDistributionsList = async () => {
industry_id: selectedIndustry.value, industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value, time_dimension: selectedTimePeriod.value,
}; };
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) { if (selectedSubCategory.value != 0) {
parms['industry_id'] = selectedSubCategory.value; parms['industry_id'] = selectedSubCategory.value;
} }
@ -202,6 +214,12 @@ const getGenderDistributionsList = async () => {
industry_id: selectedIndustry.value, industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value, time_dimension: selectedTimePeriod.value,
}; };
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) { if (selectedSubCategory.value != 0) {
parms['industry_id'] = selectedSubCategory.value; parms['industry_id'] = selectedSubCategory.value;
} }

View File

@ -0,0 +1,274 @@
<template>
<Container title="账号信息" class="container mt-24px">
<template #header>
<a-button type="outline" class="add-account-button" @click="handleAddAccount">添加子账号</a-button>
</template>
<a-table
:columns="columns"
:data="data"
:pagination="pagination"
class="mt-16px"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #mobile="{ record }">
<div class="flex item-center pt-13px pb-13px">
<span class="mr-4px">{{ record.mobile }}</span>
<a-tag v-if="record.type === 0" class="primary-account">主账号</a-tag>
<a-tag v-else class="sub-account">子账号</a-tag>
</div>
</template>
<template #action="{ record }">
<a-button
v-if="record.type !== 0"
class="delete-button"
size="mini"
type="outline"
status="danger"
@click="openDeleteModal(record)"
>
删除
</a-button>
</template>
</a-table>
<Modal v-model:visible="addAccountVisible" width="480px" title="添加子账号" :okText="okText" @ok="handleOk">
<div v-if="canAddAccount" class="add-account-container">
<h2 class="add-account-title">生成企业专属链接成员通过访问即可注册并加入企业账号</h2>
<p class="add-account-subtitle">子账号可独立登录权限继承主账号配置</p>
<div class="add-account-body">
<p>用该链接加入企业吧</p>
<p>{{ inviteUrl }}</p>
</div>
</div>
<div v-else class="add-account-container">
<h2 class="cannot-add-account-title flex item-center">
<img src="@/assets/warning.svg" alt="">
当前可用子账号数为0
</h2>
<p class="cannot-add-account-subtitle">如需添加更多子账号您可联系销售人员进行购买和权限扩展</p>
</div>
</Modal>
<CustomerServiceModal v-model:visible="customerServiceVisible" />
<DeleteModal v-model:visible="deleteVisible" :title="deleteTitle" @ok="handleDelete">
<p class="delete-modal-content">删除后该账号将无法登录您的企业</p>
</DeleteModal>
</Container>
</template>
<script setup lang="ts">
import Container from '@/components/container.vue';
import { ref, onMounted, reactive, computed } from 'vue';
import { fetchSubAccountPage, removeEnterpriseAccount, getEnterpriseInviteCode } from '@/api/all';
import Modal from '@/components/modal.vue';
import DeleteModal from '@/components/delete-modal.vue';
import CustomerServiceModal from '@/components/customer-service-modal.vue';
import { useClipboard } from '@vueuse/core';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
const store = useEnterpriseStore();
const columns = [
{
title: '手机号',
slotName: 'mobile',
},
{
title: '操作',
slotName: 'action',
},
];
const data = ref([]);
const pagination = reactive({
total: 0,
showPageSize: true,
showTotal: true,
defaultCurrent: 1,
defaultPageSize: 10,
});
const params = reactive({
page_size: 10,
page: 1,
});
const inviteUrl = ref('');
const addAccountVisible = ref(false);
const deleteVisible = ref(false);
const deleteTitle = ref('');
const enterpriseInfo = store.getEnterpriseInfo();
const okText = computed(() => {
if (!canAddAccount.value) {
return '联系客服';
}
return '复制邀请链接';
});
const customerServiceVisible = ref(false);
const canAddAccount = computed(() => {
return enterpriseInfo.sub_account_quota > enterpriseInfo.used_sub_account_count;
});
const currentSelectAccount = ref();
const { copy, copied, isSupported } = useClipboard({ source: inviteUrl });
function handlePageChange(current: number) {
params.page = current;
getSubAccount();
}
function handlePageSizeChange(pageSize: number) {
params.page_size = pageSize;
getSubAccount();
}
async function getSubAccount() {
const res = await fetchSubAccountPage(params);
pagination.total = res.total;
data.value = res.data;
}
async function handleAddAccount() {
if (canAddAccount.value) {
const res = await getEnterpriseInviteCode();
const port = window.location.port === '' ? '' : ':' + window.location.port;
const domain = window.location.protocol + '//' + window.location.hostname + port;
inviteUrl.value = domain + '?invite_code=' + res.invite_code;
}
addAccountVisible.value = true;
}
function handleOk() {
if (!canAddAccount.value) {
customerServiceVisible.value = true;
return;
}
if (!isSupported) {
AMessage.error('您的浏览器不支持复制,请手动复制!');
}
copy(inviteUrl.value);
if (!copied) {
AMessage.error('复制失败,请手动复制!');
}
AMessage.success('复制成功!');
}
function openDeleteModal(record: { id: number; mobile: string }) {
currentSelectAccount.value = record;
deleteTitle.value = `确认删除“${record.mobile}”子账号吗?`;
deleteVisible.value = true;
}
async function handleDelete() {
await removeEnterpriseAccount(currentSelectAccount.value.id);
AMessage.success('移除成功!');
}
onMounted(() => {
getSubAccount();
});
</script>
<style scoped lang="less">
.primary-account {
border-radius: 2px;
padding-right: 8px;
padding-left: 8px;
background: var(--Brand-Brand-1, rgba(240, 237, 255, 1));
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
line-height: 20px;
color: var(--Brand-Brand-6, rgba(109, 76, 254, 1));
}
.sub-account {
border-radius: 2px;
padding-right: 8px;
padding-left: 8px;
background: var(--Functional-Warning-1, rgba(255, 245, 222, 1));
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
color: var(--Functional-Warning-6, rgba(255, 174, 0, 1));
}
.delete-button {
border-radius: 4px;
padding: 2px 12px;
border: 1px solid var(--Functional-Danger-6, rgba(246, 75, 49, 1));
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
line-height: 20px;
color: var(--Functional-Danger-6, rgba(246, 75, 49, 1));
}
.add-account-button {
border-radius: 4px;
padding: 5px 16px;
border: 1px solid var(--Brand-Brand-6, rgba(109, 76, 254, 1));
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
color: var(--Brand-Brand-6, rgba(109, 76, 254, 1));
}
.add-account-container {
margin-top: 13px;
margin-bottom: 12px;
.add-account-title {
margin: 0;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
color: var(--Text-1, rgba(33, 31, 36, 1));
}
.add-account-subtitle {
margin: 4px 0 0 0;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
color: var(--Text-2, rgba(60, 64, 67, 1));
}
.add-account-body {
margin-top: 16px;
width: 432px;
height: 84px;
border-radius: 4px;
padding: 12px 16px;
background: var(--BG-200, rgba(242, 243, 245, 1));
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
color: var(--Text-2, rgba(60, 64, 67, 1));
p {
padding: 0;
margin: 0;
}
}
.cannot-add-account-title {
margin: 0;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
color: var(--Text-1, rgba(33, 31, 36, 1));
img {
width: 20px;
height: 20px;
margin-right: 12px;
}
}
.cannot-add-account-subtitle {
margin: 16px 0 0 0;
padding-left: 32px;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
color: var(--Text-2, rgba(60, 64, 67, 1));
}
}
.delete-modal-content {
margin-left: 34px;
margin-top: 16px;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
color: var(--Text-2, rgba(60, 64, 67, 1));
}
</style>

View File

@ -0,0 +1,155 @@
<template>
<Container title="企业信息" class="container mt-24px">
<a-table :columns="columns" :data="data" :pagination="false" class="mt-16px">
<template #info="{ record }">
{{ record.name }}
</template>
<template #action>
<a-button class="edit-button" size="mini" type="outline" @click="handleUpdate">修改</a-button>
</template>
</a-table>
<Modal v-model:visible="infoVisible" width="480px" title="修改企业名称" :okText="okText" @ok="handleOk">
<p class="tips">
企业名称只能修改2次请谨慎操作<span
>剩余{{ enterpriseInfo.update_name_quota - enterpriseInfo.used_update_name_count }}
</span>
</p>
<a-form
:model="form"
class="form"
:label-col-props="{ span: 6, offset: 0 }"
:wrapper-col-props="{ span: 18, offset: 0 }"
label-align="left"
>
<a-form-item required field="name" label="新企业名称">
<a-input v-model.trim="form.name" size="small" :disabled="!canUpdate" placeholder="请输入新企业名称" />
</a-form-item>
</a-form>
</Modal>
<CustomerServiceModal v-model:visible="customerServiceVisible" />
</Container>
</template>
<script setup lang="ts">
import Container from '@/components/container.vue';
import Modal from '@/components/modal.vue';
import { ref, reactive, computed } from 'vue';
import CustomerServiceModal from '@/components/customer-service-modal.vue';
import { updateEnterpriseName } from '@/api/all';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
const store = useEnterpriseStore();
const form = reactive({
name: '',
});
const enterpriseInfo = store.getEnterpriseInfo();
const columns = [
{
title: '企业名称',
slotName: 'info',
},
{
title: '操作',
slotName: 'action',
},
];
const data = ref([enterpriseInfo]);
const infoVisible = ref(false);
const customerServiceVisible = ref(false);
const canUpdate = computed(() => {
return enterpriseInfo.update_name_quota > enterpriseInfo.used_update_name_count;
});
const okText = computed(() => {
if (!canUpdate.value) {
return '联系客服';
}
return '确定';
});
function handleUpdate() {
if (!canUpdate.value) {
form.name = enterpriseInfo.name;
}
infoVisible.value = true;
}
async function handleOk() {
if (!canUpdate.value) {
customerServiceVisible.value = true;
return;
}
await updateEnterpriseName({ name: form.name });
store.setEnterpriseName(form.name);
store.incUsedUpdateNameCount();
AMessage.success('修改成功!');
}
</script>
<style scoped lang="less">
.tips {
height: 40px;
border-radius: 4px;
padding: 10px 16px;
background: var(--BG-100, rgba(247, 248, 250, 1));
border: 1px solid var(--BG-300, rgba(230, 230, 232, 1));
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
span {
color: var(--Functional-Danger-6, rgba(246, 75, 49, 1));
}
}
.form {
margin-top: 20px;
:deep(.arco-row) {
align-items: center;
}
:deep(.arco-form-item-label) {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
margin: 0;
}
:deep(.arco-input-wrapper) {
background: white;
border: 1px solid var(--BG-400, rgba(215, 215, 217, 1));
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
padding: 4px 12px;
input::placeholder {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
color: var(--Text-4, rgba(147, 148, 153, 1));
}
}
:deep(.arco-input-disabled) {
background: var(--BG-200, rgba(242, 243, 245, 1));
border: 1px solid var(--BG-400, rgba(215, 215, 217, 1));
.arco-input:disabled {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
}
}
:deep(.arco-input-focus) {
border: 1px solid var(--Brand-Brand-6, rgba(109, 76, 254, 1));
box-shadow: 0 2px 4px 0 rgba(109, 76, 254, 0.2);
}
}
.edit-button {
margin: 12px 0;
border: 1px solid rgba(109, 76, 254, 1);
border-radius: 4px;
padding: 2px 12px;
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
color: rgba(109, 76, 254, 1);
}
</style>

View File

@ -0,0 +1,305 @@
<template>
<Container title="个人信息" class="container mt-24px">
<a-table :columns="columns" :data="data" :pagination="false" class="mt-16px">
<template #info="{ record }">
<div class="pt-3px pb-3px">
<a-avatar :image-url="record.head_image" :size="32" />
{{ record.name }}
<icon-edit size="13" class="ml-8px" @click="openEditInfoModal" />
</div>
</template>
<template #mobile="{ record }">
{{ record.mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') }}
<icon-edit size="13" class="ml-8px" @click="openEditMobileModal" />
</template>
</a-table>
<Modal v-model:visible="infoVisible" title="修改用户信息" @ok="handleSubmitUserInfo">
<a-form
class="form"
:model="userInfoForm"
:label-col-props="{ span: 3, offset: 0 }"
:wrapper-col-props="{ span: 21, offset: 0 }"
>
<a-form-item field="head_image" label="头像">
<div class="flex item-center">
<a-avatar :image-url="userInfoForm.file_url" :size="48" />
<span class="upload-button" @click="triggerFileInput">
<input
ref="uploadInputRef"
accept="image/*"
type="file"
style="display: none"
@change="handleFileChange"
/>
<a-button><icon-upload />上传新头像</a-button>
</span>
</div>
</a-form-item>
<a-form-item field="name" label="昵称">
<a-input v-model.trim="userInfoForm.name" placeholder="请输入昵称" />
</a-form-item>
</a-form>
</Modal>
<Modal v-model:visible="imageVisible" title="头像裁剪">
<VueCropper></VueCropper>
</Modal>
<Modal v-model:visible="mobileVisible" title="修改手机号" @ok="handleUpdateMobile">
<a-form
ref="formRef"
:model="form"
class="form"
:rules="formRules"
:label-col-props="{ span: 5, offset: 0 }"
:wrapper-col-props="{ span: 19, offset: 0 }"
label-align="left"
>
<a-form-item required field="mobile" label="新手机号">
<a-input v-model.trim="form.mobile" size="small" placeholder="请输入新的手机号" />
</a-form-item>
<a-form-item required field="captcha" label="获取验证码">
<a-input v-model.trim="form.captcha" size="small" placeholder="请输入验证码">
<template #suffix>
<span v-if="countdown <= 0" @click="sendCaptcha">发送验证码</span>
<span v-else>{{ countdown }}s</span>
</template>
</a-input>
</a-form-item>
</a-form>
<PuzzleVerification
:show="verificationVisible"
@submit="handleVerificationSubmit"
@cancel="verificationVisible = false"
/>
</Modal>
</Container>
</template>
<script setup lang="ts">
import Container from '@/components/container.vue';
import Modal from '@/components/modal.vue';
import PuzzleVerification from '@/views/components/login/PuzzleVerification.vue';
import { ref, reactive } from 'vue';
import 'vue-cropper/dist/index.css';
import { VueCropper } from 'vue-cropper';
import { sendUpdateMobileCaptcha, updateMobile, fetchImageUploadFile, updateMyInfo } from '@/api/all';
import axios from 'axios';
import { useUserStore } from '@/stores';
const store = useUserStore();
const userInfo = store.getUserInfo();
const columns = [
{
title: '用户信息',
slotName: 'info',
},
{
title: '手机号',
slotName: 'mobile',
},
];
const data = reactive([userInfo]);
const infoVisible = ref(false);
const imageVisible = ref(false);
const mobileVisible = ref(false);
const verificationVisible = ref(false);
const timer = ref();
const countdown = ref(0);
const formRef = ref();
const isSendCaptcha = ref(false);
const uploadInputRef = ref();
// 表单校验规则
const formRules = {
mobile: [
{
required: true,
message: '请填写手机号',
trigger: ['blur', 'change'],
},
{
validator: (value: string, callback: (error?: string) => void) => {
if (!/^1[3-9]\d{9}$/.test(value)) {
callback('手机号格式不正确');
} else {
callback();
}
},
trigger: ['blur', 'change'],
},
],
captcha: [
{
required: true,
message: '请填写验证码',
trigger: ['blur', 'change'],
},
{
validator: (value: string, callback: (error?: string) => void) => {
if (!/^\d{6}$/.test(value)) {
callback('验证码必须是6位数字');
} else {
callback();
}
},
trigger: ['blur', 'change'],
},
],
};
const form = reactive({
mobile: '',
captcha: '',
});
const userInfoForm = reactive({
name: '',
head_image: '',
file_url: '',
});
function triggerFileInput() {
uploadInputRef.value.click();
}
function openEditInfoModal() {
infoVisible.value = true;
}
async function handleFileChange(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
const fileExtension = getFileExtension(file.name);
const res = await fetchImageUploadFile({
suffix: fileExtension,
});
const blob = new Blob([file], { type: file.type });
await axios.put(res.upload_url, blob, {
headers: { 'Content-Type': file.type },
});
userInfoForm.head_image = res.file_name;
userInfoForm.file_url = res.file_url;
}
}
function openEditImageModal() {
imageVisible.value = true;
}
function openEditMobileModal() {
mobileVisible.value = true;
}
async function handleSubmitUserInfo() {
await updateMyInfo(userInfoForm);
store.setUserName(userInfoForm.name);
store.setUserHeadImage(userInfoForm.file_url);
AMessage.success('修改成功!');
}
async function sendCaptcha() {
try {
const result = await formRef.value.validateField('mobile');
if (result === true || result === undefined) {
verificationVisible.value = true;
isSendCaptcha.value = true;
}
AMessage.error('请填写正确的手机号!');
} catch (error) {
console.log('手机号验证失败:', error);
}
}
function beginCountdown() {
timer.value = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer.value);
timer.value = null;
}
}, 1000);
}
async function handleVerificationSubmit() {
await sendUpdateMobileCaptcha({ mobile: form.mobile });
AMessage.success('发送成功');
verificationVisible.value = false;
countdown.value = 60;
beginCountdown();
}
async function handleUpdateMobile() {
if (!isSendCaptcha.value) {
AMessage.error('请先获取验证码!');
return false;
}
const res = await formRef.value.validate();
if (res === true || res === undefined) {
await updateMobile(form);
store.setUserMobile(form.mobile);
AMessage.success('修改成功!');
}
}
function getFileExtension(filename: string): string {
const match = filename.match(/\.([^.]+)$/);
return match ? match[1].toLowerCase() : '';
}
</script>
<style scoped lang="less">
.form {
margin-top: 13px;
:deep(.arco-row) {
align-items: center;
}
:deep(.arco-form-item-label) {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
margin: 0;
}
:deep(.arco-input-wrapper) {
background: white;
border: 1px solid var(--BG-400, rgba(215, 215, 217, 1));
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
padding: 4px 12px;
input::placeholder {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
color: var(--Text-4, rgba(147, 148, 153, 1));
}
}
:deep(.arco-input-disabled) {
background: var(--BG-200, rgba(242, 243, 245, 1));
border: 1px solid var(--BG-400, rgba(215, 215, 217, 1));
.arco-input:disabled {
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 14px;
}
}
:deep(.arco-input-focus) {
border: 1px solid var(--Brand-Brand-6, rgba(109, 76, 254, 1));
box-shadow: 0 2px 4px 0 rgba(109, 76, 254, 0.2);
}
}
.upload-button {
width: 104px;
height: 24px;
margin-left: 12px;
:deep(.arco-btn) {
width: 104px;
height: 24px;
border-radius: 4px;
padding: 2px 12px;
border: 1px solid var(--BG-500, rgba(177, 178, 181, 1));
font-family: Alibaba PuHuiTi, serif;
font-weight: 400;
font-size: 12px;
vertical-align: middle;
color: var(--Text-2, rgba(60, 64, 67, 1));
}
}
</style>

View File

@ -21,7 +21,7 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Container from '@/views/components/workplace/modules/container.vue'; import Container from '@/components/container.vue';
import Product from '@/views/components/workplace/modules/product.vue'; import Product from '@/views/components/workplace/modules/product.vue';
import Case from '@/views/components/workplace/modules/case.vue'; import Case from '@/views/components/workplace/modules/case.vue';
import { fetchProductList, fetchSuccessCaseList } from '@/api/all/index'; import { fetchProductList, fetchSuccessCaseList } from '@/api/all/index';
@ -44,8 +44,8 @@ const getSuccessCaseList = async () => {
<style scoped lang="less"> <style scoped lang="less">
.body { .body {
padding-left: 0; padding-left: 4px !important;
:deep(> .title) { :deep(> div > .title) {
padding-left: 20px; padding-left: 20px;
} }
} }

View File

@ -65,14 +65,7 @@
</a-button> </a-button>
</a-popconfirm> </a-popconfirm>
</div> </div>
<a-modal v-model:visible="visible"> <CustomerServiceModal v-model:visible="visible" />
<template #title>
扫描下面二维码联系客户
</template>
<div class="text-center">
<img width="200" src="@/assets/customer-service.svg" alt="" />
</div>
</a-modal>
</div> </div>
</template> </template>
@ -80,6 +73,8 @@
import { now } from '@vueuse/core'; import { now } from '@vueuse/core';
import { trialProduct } from '@/api/all'; import { trialProduct } from '@/api/all';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import CustomerServiceModal from '@/components/customer-service-modal.vue';
const props = defineProps<{ const props = defineProps<{
product: Product; product: Product;
}>(); }>();

View File

@ -4911,6 +4911,11 @@ vm2@^3.9.8:
acorn "^8.7.0" acorn "^8.7.0"
acorn-walk "^8.2.0" acorn-walk "^8.2.0"
vue-cropper@^1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/vue-cropper/-/vue-cropper-1.1.4.tgz"
integrity sha512-5m98vBsCEI9rbS4JxELxXidtAui3qNyTHLHg67Qbn7g8cg+E6LcnC+hh3SM/p94x6mFh6KRxT1ttnta+wCYqWA==
vue-demi@*, vue-demi@^0.13.11: vue-demi@*, vue-demi@^0.13.11:
version "0.13.11" version "0.13.11"
resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz" resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz"