feat(customer-service): 新增客户服务问题反馈管理功能

- 重命名语言文件中相关关键字,统一改为 customer-service_feedbacks
- 新增客户服务反馈页面路由及组件导入,移除旧的 www_support_feedbacks 相关路由
- 实现客户服务反馈列表及详情查看,支持状态筛选、分页查询和消息对话展示
- 支持反馈状态变更,包括待处理、处理中和已解决状态切换
- 支持在对话中发送回复消息,且针对已解决状态自动停止轮询
- 优化状态标签样式,采用更易区分的颜色与视觉样式
- 组件及类型定义中新增 NCollapse 和 NCollapseItem 支持
- 移除旧的 www 支持反馈组件代码,清理无用路由及类型定义相关项
This commit is contained in:
eafonyang
2026-08-25 14:18:47 +08:00
parent 3702ad4163
commit f60255574e
10 changed files with 236 additions and 105 deletions
@@ -272,7 +272,7 @@ const local: App.I18n.Schema = {
www_contact_settings: 'Form Settings',
www_contact_submissions: 'Submissions',
'customer-service': 'Customer Service',
www_support_feedbacks: 'Support Feedback',
'customer-service_feedbacks': 'Support Feedback',
'www_page-seo': 'Page SEO'
},
page: {
@@ -268,8 +268,8 @@ const local: App.I18n.Schema = {
www_contact: '联系表单',
www_contact_settings: '表单配置',
www_contact_submissions: '提交记录',
'customer-service': '客服务',
www_support_feedbacks: '问题反馈',
'customer-service': '客服务',
'customer-service_feedbacks': '问题反馈',
'www_page-seo': '页面 SEO'
},
page: {
@@ -20,6 +20,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
500: () => import("@/views/_builtin/500/index.vue"),
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
login: () => import("@/views/_builtin/login/index.vue"),
"customer-service_feedbacks": () => import("@/views/customer-service/feedbacks/index.vue"),
headphone_brand: () => import("@/views/headphone/brand/index.vue"),
headphone_impedance: () => import("@/views/headphone/impedance/index.vue"),
headphone_model: () => import("@/views/headphone/model/index.vue"),
@@ -51,5 +52,4 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
www_product_detail: () => import("@/views/www/product/detail/index.vue"),
www_product_list: () => import("@/views/www/product/list/index.vue"),
www_product_series: () => import("@/views/www/product/series/index.vue"),
www_support_feedbacks: () => import("@/views/www/support/feedbacks/index.vue"),
};
+25 -25
View File
@@ -39,6 +39,31 @@ export const generatedRoutes: GeneratedRoute[] = [
hideInMenu: true
}
},
{
name: 'customer-service',
path: '/customer-service',
component: 'layout.base',
meta: {
title: 'customer-service',
i18nKey: 'route.customer-service',
icon: 'mdi:headset',
order: 7
},
children: [
{
name: 'customer-service_feedbacks',
path: '/customer-service/feedbacks',
component: 'view.customer-service_feedbacks',
meta: {
title: 'customer-service_feedbacks',
i18nKey: 'route.customer-service_feedbacks',
icon: 'mdi:message-alert-outline',
order: 1,
keepAlive: true
}
}
]
},
{
name: 'headphone',
path: '/headphone',
@@ -585,31 +610,6 @@ export const generatedRoutes: GeneratedRoute[] = [
}
}
]
},
]
},
{
name: 'customer-service',
path: '/www/support',
component: 'layout.base',
meta: {
title: 'customer-service',
i18nKey: 'route.customer-service',
icon: 'mdi:headset',
order: 7
},
children: [
{
name: 'www_support_feedbacks',
path: '/www/support/feedbacks',
component: 'view.www_support_feedbacks',
meta: {
title: 'www_support_feedbacks',
i18nKey: 'route.www_support_feedbacks',
icon: 'mdi:message-alert-outline',
order: 1,
keepAlive: true
}
}
]
}
@@ -166,6 +166,8 @@ const routeMap: RouteMap = {
"403": "/403",
"404": "/404",
"500": "/500",
"customer-service": "/customer-service",
"customer-service_feedbacks": "/customer-service/feedbacks",
"headphone": "/headphone",
"headphone_brand": "/headphone/brand",
"headphone_impedance": "/headphone/impedance",
@@ -210,9 +212,7 @@ const routeMap: RouteMap = {
"www_product": "/www/product",
"www_product_detail": "/www/product/detail",
"www_product_list": "/www/product/list",
"www_product_series": "/www/product/series",
"customer-service": "/www/support",
"www_support_feedbacks": "/www/support/feedbacks"
"www_product_series": "/www/product/series"
};
/**
+4
View File
@@ -41,6 +41,8 @@ declare module 'vue' {
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NDataTable: typeof import('naive-ui')['NDataTable']
NDatePicker: typeof import('naive-ui')['NDatePicker']
@@ -132,6 +134,8 @@ declare global {
const NButton: typeof import('naive-ui')['NButton']
const NCard: typeof import('naive-ui')['NCard']
const NCheckbox: typeof import('naive-ui')['NCheckbox']
const NCollapse: typeof import('naive-ui')['NCollapse']
const NCollapseItem: typeof import('naive-ui')['NCollapseItem']
const NColorPicker: typeof import('naive-ui')['NColorPicker']
const NDataTable: typeof import('naive-ui')['NDataTable']
const NDatePicker: typeof import('naive-ui')['NDatePicker']
+4 -4
View File
@@ -20,6 +20,8 @@ declare module "@elegant-router/types" {
"403": "/403";
"404": "/404";
"500": "/500";
"customer-service": "/customer-service";
"customer-service_feedbacks": "/customer-service/feedbacks";
"headphone": "/headphone";
"headphone_brand": "/headphone/brand";
"headphone_impedance": "/headphone/impedance";
@@ -65,8 +67,6 @@ declare module "@elegant-router/types" {
"www_product_detail": "/www/product/detail";
"www_product_list": "/www/product/list";
"www_product_series": "/www/product/series";
"customer-service": "/www/support";
"www_support_feedbacks": "/www/support/feedbacks";
};
/**
@@ -101,6 +101,7 @@ declare module "@elegant-router/types" {
| "403"
| "404"
| "500"
| "customer-service"
| "headphone"
| "home"
| "iframe-page"
@@ -111,7 +112,6 @@ declare module "@elegant-router/types" {
| "toolbox"
| "upgrade"
| "www"
| "customer-service"
>;
/**
@@ -133,6 +133,7 @@ declare module "@elegant-router/types" {
| "500"
| "iframe-page"
| "login"
| "customer-service_feedbacks"
| "headphone_brand"
| "headphone_impedance"
| "headphone_model"
@@ -164,7 +165,6 @@ declare module "@elegant-router/types" {
| "www_product_detail"
| "www_product_list"
| "www_product_series"
| "www_support_feedbacks"
>;
/**
@@ -8,7 +8,7 @@ import {
fetchUpdateSupportFeedbackStatus,
} from '@/service/api';
defineOptions({ name: 'WwwSupportFeedbacks' });
defineOptions({ name: 'CustomerServiceFeedbacks' });
const POLL_INTERVAL_MS = 4000;
+194 -67
View File
@@ -170,10 +170,30 @@ function statusLabel(status: string): string {
return t('support.statusPending');
}
function statusClass(status: string): string {
if (status === 'resolved') return 'bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-300 dark:border-green-800';
if (status === 'processing') return 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800';
return 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-900/20 dark:text-amber-300 dark:border-amber-800';
function statusMeta(status: string): { badge: string; accent: string; dot: string } {
if (status === 'resolved') {
return {
badge: 'bg-emerald-50 text-emerald-700 ring-1 ring-emerald-200/80 dark:bg-emerald-950/40 dark:text-emerald-300 dark:ring-emerald-800/60',
accent: 'bg-emerald-500',
dot: 'bg-emerald-500',
};
}
if (status === 'processing') {
return {
badge: 'bg-sky-50 text-sky-700 ring-1 ring-sky-200/80 dark:bg-sky-950/40 dark:text-sky-300 dark:ring-sky-800/60',
accent: 'bg-sky-500',
dot: 'bg-sky-500',
};
}
return {
badge: 'bg-amber-50 text-amber-700 ring-1 ring-amber-200/80 dark:bg-amber-950/40 dark:text-amber-300 dark:ring-amber-800/60',
accent: 'bg-amber-500',
dot: 'bg-amber-500',
};
}
function isExpanded(item: SupportFeedbackItem): boolean {
return expandedFeedbackNo.value === item.feedback_no;
}
function formatTime(iso: string): string {
@@ -334,91 +354,198 @@ function handleStatusUpdate(feedbackNo: string, status: string) {
</form>
<!-- 我的反馈查询 -->
<div class="flex flex-col gap-16px p-24px rounded-12px border border-gray-100 dark:border-dark-700 bg-white dark:bg-dark-800">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('support.myFeedback') }}</h3>
<div class="feedback-panel flex flex-col gap-20px p-24px md:p-28px rounded-16px border border-gray-200/80 dark:border-dark-600/80 bg-white dark:bg-dark-800 shadow-sm">
<div class="flex flex-col gap-6px">
<h3 class="text-lg md:text-xl font-semibold text-gray-900 dark:text-white tracking-tight">
{{ t('support.myFeedback') }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('support.queryPlaceholder') }}
</p>
</div>
<div class="flex gap-8px">
<input
v-model="queryInput"
type="text"
:placeholder="t('support.queryPlaceholder')"
class="flex-1 min-w-0 px-16px py-12px rounded-8px border text-sm bg-white dark:bg-dark-800 dark:text-gray-100 outline-none transition-colors focus:border-primary"
:class="queryError ? 'border-red-400' : 'border-gray-200 dark:border-dark-600'"
@keyup.enter="handleQuery"
@input="queryError = ''"
>
<div class="flex flex-col sm:flex-row gap-10px">
<div class="relative flex-1 min-w-0">
<span class="feedback-search-icon pointer-events-none absolute left-14px top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-18px h-18px">
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11ZM2 9a7 7 0 1 1 12.452 4.391l3.328 3.329a.75.75 0 1 1-1.06 1.06l-3.329-3.328A7 7 0 0 1 2 9Z" clip-rule="evenodd" />
</svg>
</span>
<input
v-model="queryInput"
type="text"
:placeholder="t('support.queryPlaceholder')"
class="feedback-search-input w-full pl-42px pr-16px py-13px rounded-12px border text-sm bg-gray-50/80 dark:bg-dark-900/50 dark:text-gray-100 outline-none transition-all placeholder:text-gray-400 dark:placeholder:text-gray-500"
:class="queryError
? 'border-red-400 focus:border-red-400 focus:ring-2 focus:ring-red-400/20'
: 'border-gray-200 dark:border-dark-600 focus:border-primary focus:ring-2 focus:ring-primary/15'"
@keyup.enter="handleQuery"
@input="queryError = ''"
>
</div>
<button
type="button"
:disabled="querying"
class="px-24px py-12px rounded-8px bg-primary text-white text-sm font-medium hover:opacity-90 transition-opacity disabled:opacity-50 self-start"
class="feedback-query-btn shrink-0 px-28px py-13px rounded-12px bg-primary text-white text-sm font-medium shadow-sm hover:shadow-md hover:opacity-95 active:scale-[0.98] transition-all disabled:opacity-50 disabled:cursor-not-allowed"
@click="handleQuery"
>
{{ querying ? t('common.loading') : t('support.query') }}
</button>
</div>
<p v-if="queryError" class="text-xs text-red-500">{{ queryError }}</p>
<p v-if="queryError" class="text-xs text-red-500 -mt-8px">{{ queryError }}</p>
<div v-if="queried && !querying && feedbacks.length === 0" class="py-32px text-center text-sm text-gray-400">
{{ t('support.noFeedback') }}
<div
v-if="queried && !querying && feedbacks.length === 0"
class="feedback-empty flex flex-col items-center justify-center gap-12px py-48px px-24px rounded-12px border border-dashed border-gray-200 dark:border-dark-600 bg-gray-50/50 dark:bg-dark-900/30"
>
<div class="w-48px h-48px rounded-full bg-gray-100 dark:bg-dark-700 flex items-center justify-center text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-24px h-24px">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
</svg>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('support.noFeedback') }}</p>
</div>
<div v-else class="flex flex-col gap-12px">
<div
<div v-else-if="feedbacks.length > 0" class="flex flex-col gap-14px">
<article
v-for="item in feedbacks"
:key="item.id"
class="flex flex-col gap-8px p-16px rounded-8px border border-gray-100 dark:border-dark-700 bg-gray-50/50 dark:bg-dark-700/40"
class="feedback-card group relative overflow-hidden rounded-14px border transition-all duration-200"
:class="isExpanded(item)
? 'border-primary/40 dark:border-primary/30 bg-white dark:bg-dark-800 shadow-md ring-1 ring-primary/10'
: 'border-gray-200/90 dark:border-dark-600/80 bg-white dark:bg-dark-800/90 hover:border-gray-300 dark:hover:border-dark-500 hover:shadow-sm'"
>
<button
type="button"
class="flex flex-col gap-8px text-left w-full"
@click="toggleExpand(item)"
>
<div class="flex items-center justify-between gap-12px">
<span class="font-medium text-sm text-gray-900 dark:text-white truncate">{{ productName(item) }}</span>
<span class="shrink-0 px-10px py-2px rounded-full border text-xs" :class="statusClass(item.status)">
<span
class="feedback-card-accent absolute left-0 top-0 bottom-0 w-3px"
:class="statusMeta(item.status).accent"
aria-hidden="true"
/>
<div class="p-18px md:p-20px pl-22px md:pl-24px flex flex-col gap-14px">
<div class="flex items-start justify-between gap-16px">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-10px mb-8px">
<span class="feedback-product-icon shrink-0 w-36px h-36px rounded-10px bg-primary/10 text-primary flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-18px h-18px">
<path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-7-4a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM9 9a.75.75 0 0 0 0 1.5h.253a.25.25 0 0 1 .244.304l-.459 2.066A1.75 1.75 0 0 0 10.747 15H11a.75.75 0 0 0 0-1.5h-.253a.25.25 0 0 1-.244-.304l.459-2.066A1.75 1.75 0 0 0 9.253 9H9Z" clip-rule="evenodd" />
</svg>
</span>
<h4 class="font-semibold text-base text-gray-900 dark:text-white truncate">
{{ productName(item) }}
</h4>
</div>
<div class="flex flex-wrap items-center gap-x-10px gap-y-6px text-xs text-gray-500 dark:text-gray-400">
<time>{{ formatTime(item.created_at) }}</time>
<template v-if="item.feedback_no">
<span class="text-gray-300 dark:text-gray-600">|</span>
<button
type="button"
class="feedback-no-chip inline-flex items-center gap-6px font-mono text-primary hover:text-primary/80 transition-colors"
:title="t('support.feedbackNoCopied')"
@click="copyFeedbackNo(item.feedback_no)"
>
{{ item.feedback_no }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-14px h-14px shrink-0 text-primary">
<path d="M7 3.5A1.5 1.5 0 0 1 8.5 2h3.879a1.5 1.5 0 0 1 1.06.44l3.122 3.12A1.5 1.5 0 0 1 17 6.622V12.5a1.5 1.5 0 0 1-1.5 1.5h-1v-3.379a3 3 0 0 0-.879-2.121L10.5 5.379A3 3 0 0 0 8.379 4.5H7v-1Z" />
<path d="M4.5 6A1.5 1.5 0 0 0 3 7.5v9A1.5 1.5 0 0 0 4.5 18h7a1.5 1.5 0 0 0 1.5-1.5v-5.879a1.5 1.5 0 0 0-.44-1.06L9.44 6.439A1.5 1.5 0 0 0 8.378 6H4.5Z" />
</svg>
</button>
<span
v-if="copied && copiedNo === item.feedback_no"
class="text-emerald-600 dark:text-emerald-400"
>
{{ t('support.feedbackNoCopied') }}
</span>
</template>
</div>
</div>
<span
class="feedback-status shrink-0 inline-flex items-center gap-6px px-10px py-4px rounded-full text-xs font-medium"
:class="statusMeta(item.status).badge"
>
<span class="w-6px h-6px rounded-full" :class="statusMeta(item.status).dot" />
{{ statusLabel(item.status) }}
</span>
</div>
<div class="flex items-center gap-8px text-xs text-gray-400">
<span>{{ formatTime(item.created_at) }}</span>
<template v-if="item.feedback_no">
<span>·</span>
<span
class="font-mono text-primary hover:underline"
@click.stop="copyFeedbackNo(item.feedback_no)"
>
{{ item.feedback_no }}
</span>
<span v-if="copied && copiedNo === item.feedback_no" class="text-green-600 dark:text-green-400">
{{ t('support.feedbackNoCopied') }}
</span>
</template>
<span class="ml-auto text-primary">{{ expandedFeedbackNo === item.feedback_no ? t('support.collapseChat') : t('support.expandChat') }}</span>
</div>
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed whitespace-pre-line break-words line-clamp-2">{{ item.description }}</p>
</button>
<a
v-if="item.log_file_path"
:href="item.log_file_path"
target="_blank"
rel="noopener noreferrer"
class="text-xs text-primary hover:underline self-start"
@click.stop
>
📎 {{ item.log_file_name }}
</a>
<SupportFeedbackChat
v-if="expandedFeedbackNo === item.feedback_no"
:item="item"
:verified-email="verifiedEmailFor(item)"
@verified="(email) => handleEmailVerified(item.feedback_no, email)"
@status-update="(status) => handleStatusUpdate(item.feedback_no, status)"
/>
</div>
<div class="rounded-10px bg-gray-50/90 dark:bg-dark-900/45 px-14px py-12px border border-gray-100/80 dark:border-dark-600/60 flex items-start gap-12px">
<p
class="flex-1 min-w-0 text-sm text-gray-700 dark:text-gray-300 leading-relaxed whitespace-pre-line break-words"
:class="isExpanded(item) ? '' : 'line-clamp-2'"
>
{{ item.description }}
</p>
<button
type="button"
class="feedback-toggle-btn shrink-0 self-center inline-flex items-center gap-4px px-12px py-7px rounded-8px text-xs font-medium transition-all"
:class="isExpanded(item)
? 'bg-primary/10 text-primary'
: 'bg-white text-gray-700 ring-1 ring-gray-200/90 hover:bg-primary/10 hover:text-primary hover:ring-primary/20 dark:bg-dark-700 dark:text-gray-200 dark:ring-dark-500 dark:hover:bg-primary/15 dark:hover:text-primary'"
@click="toggleExpand(item)"
>
{{ isExpanded(item) ? t('support.collapseChat') : t('support.expandChat') }}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-14px h-14px transition-transform duration-200"
:class="isExpanded(item) ? 'rotate-180' : ''"
>
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.94a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z" clip-rule="evenodd" />
</svg>
</button>
</div>
<a
v-if="item.log_file_path"
:href="item.log_file_path"
target="_blank"
rel="noopener noreferrer"
class="feedback-attachment inline-flex items-center gap-6px max-w-full self-start px-10px py-6px rounded-8px text-xs text-gray-600 dark:text-gray-300 bg-gray-100/80 dark:bg-dark-700/80 hover:bg-gray-200/80 dark:hover:bg-dark-600/80 transition-colors truncate"
@click.stop
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-14px h-14px shrink-0 text-gray-400">
<path fill-rule="evenodd" d="m15.988 3.012-8.25 8.25a2.25 2.25 0 1 1-3.182-3.182l8.25-8.25a3.75 3.75 0 0 1 5.303 5.303l-8.25 8.25a5.25 5.25 0 1 1-7.424-7.424l8.25-8.25a.75.75 0 1 1 1.06 1.06l-8.25 8.25a3.75 3.75 0 0 0 5.303 5.303l8.25-8.25a2.25 2.25 0 0 0-3.182-3.182l-8.25 8.25a.75.75 0 1 1-1.06-1.06l8.25-8.25a3.75 3.75 0 0 0-5.303-5.303Z" clip-rule="evenodd" />
</svg>
<span class="truncate">{{ item.log_file_name }}</span>
</a>
<SupportFeedbackChat
v-if="isExpanded(item)"
:item="item"
:verified-email="verifiedEmailFor(item)"
@verified="(email) => handleEmailVerified(item.feedback_no, email)"
@status-update="(status) => handleStatusUpdate(item.feedback_no, status)"
/>
</div>
</article>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.feedback-panel {
background-image: linear-gradient(180deg, rgb(255 255 255 / 1) 0%, rgb(249 250 251 / 0.55) 100%);
}
:root.dark .feedback-panel,
.dark .feedback-panel {
background-image: linear-gradient(180deg, rgb(30 30 35 / 1) 0%, rgb(24 24 28 / 0.92) 100%);
}
.feedback-card-accent {
opacity: 0.92;
}
.feedback-no-chip:focus-visible,
.feedback-toggle-btn:focus-visible,
.feedback-query-btn:focus-visible,
.feedback-search-input:focus-visible {
outline: none;
}
</style>
+1 -1
View File
@@ -143,7 +143,7 @@ onUnmounted(stopPolling);
</script>
<template>
<div class="mt-12px pt-12px border-t border-gray-100 dark:border-dark-600 flex flex-col gap-12px">
<div class="mt-4px pt-16px border-t border-gray-100 dark:border-dark-600 flex flex-col gap-12px">
<div v-if="!effectiveEmail" class="flex flex-col gap-8px">
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('support.verifyEmailHint') }}</p>
<div class="flex gap-8px">