feat: 批量邮件记录 Handsontable 查询与近30天软拦防重
All checks were successful
Build and Deploy Front / deploy (push) Successful in 9m36s
All checks were successful
Build and Deploy Front / deploy (push) Successful in 9m36s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0910538e6b
commit
338374397c
@ -167,8 +167,37 @@ export async function sendBatchEmailOne(body: BatchEmailSendOneBody) {
|
|||||||
assertBusinessOk(res.data)
|
assertBusinessOk(res.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BatchEmailDuplicatePair {
|
||||||
|
to: string
|
||||||
|
subject: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchEmailDuplicateHit {
|
||||||
|
toEmail: string
|
||||||
|
subject: string
|
||||||
|
sendTime: string
|
||||||
|
sender: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDuplicateHit(raw: unknown): BatchEmailDuplicateHit | null {
|
||||||
|
if (raw == null || typeof raw !== 'object') return null
|
||||||
|
const row = raw as Record<string, unknown>
|
||||||
|
const toEmail = pickStr(row, 'toEmail', 'ToEmail')
|
||||||
|
if (!toEmail) return null
|
||||||
|
return {
|
||||||
|
toEmail,
|
||||||
|
subject: pickStr(row, 'subject', 'Subject'),
|
||||||
|
sendTime: pickStr(row, 'sendTime', 'SendTime'),
|
||||||
|
sender: pickStr(row, 'sender', 'Sender'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** GET /system/batch-email/records/list */
|
/** GET /system/batch-email/records/list */
|
||||||
export async function listBatchEmailRecords(params?: { page?: number; pageSize?: number }) {
|
export async function listBatchEmailRecords(params?: {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
|
}) {
|
||||||
const res = await axiosInstance.get(omsUrl('/system/batch-email/records/list'), { params })
|
const res = await axiosInstance.get(omsUrl('/system/batch-email/records/list'), { params })
|
||||||
const page = unwrapPageResult(res.data)
|
const page = unwrapPageResult(res.data)
|
||||||
return {
|
return {
|
||||||
@ -178,3 +207,15 @@ export async function listBatchEmailRecords(params?: { page?: number; pageSize?:
|
|||||||
total: page.total,
|
total: page.total,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** POST /system/batch-email/check-duplicates — 近 N 天同邮箱同主题预检 */
|
||||||
|
export async function checkBatchEmailDuplicates(body: {
|
||||||
|
pairs: BatchEmailDuplicatePair[]
|
||||||
|
withinDays?: number
|
||||||
|
}): Promise<BatchEmailDuplicateHit[]> {
|
||||||
|
const res = await axiosInstance.post(omsUrl('/system/batch-email/check-duplicates'), body)
|
||||||
|
const list = unwrapList(res.data)
|
||||||
|
return list
|
||||||
|
.map(normalizeDuplicateHit)
|
||||||
|
.filter((item): item is BatchEmailDuplicateHit => item != null)
|
||||||
|
}
|
||||||
|
|||||||
@ -4399,11 +4399,24 @@ export default {
|
|||||||
'system.batchEmail.errLoadTemplates': 'Failed to load templates',
|
'system.batchEmail.errLoadTemplates': 'Failed to load templates',
|
||||||
'system.batchEmail.errLoadRecords': 'Failed to load email records',
|
'system.batchEmail.errLoadRecords': 'Failed to load email records',
|
||||||
'system.batchEmail.recordList': 'Email records',
|
'system.batchEmail.recordList': 'Email records',
|
||||||
|
'system.batchEmail.recordSearch': 'Search',
|
||||||
|
'system.batchEmail.recordSearchPlaceholder': 'Email / subject / body / sender',
|
||||||
'system.batchEmail.record.toEmail': 'Email',
|
'system.batchEmail.record.toEmail': 'Email',
|
||||||
'system.batchEmail.record.subject': 'Subject',
|
'system.batchEmail.record.subject': 'Subject',
|
||||||
'system.batchEmail.record.content': 'Content',
|
'system.batchEmail.record.content': 'Content',
|
||||||
'system.batchEmail.record.sendTime': 'Sent at',
|
'system.batchEmail.record.sendTime': 'Sent at',
|
||||||
'system.batchEmail.record.sender': 'Sender',
|
'system.batchEmail.record.sender': 'Sender',
|
||||||
|
'system.batchEmail.errCheckDup': 'Duplicate check failed. Please try again.',
|
||||||
|
'system.batchEmail.dupConfirmTitle': 'Duplicates in the last 30 days',
|
||||||
|
'system.batchEmail.dupConfirmBody': '{count} item(s) share the same email + subject within {days} days (including in-batch duplicates):',
|
||||||
|
'system.batchEmail.dupConfirmMore': '{n} more not shown',
|
||||||
|
'system.batchEmail.dupConfirmBatchOnly': '{n} more are duplicates within this batch',
|
||||||
|
'system.batchEmail.dupBatchOnlyTip': '{n} duplicate group(s) in this batch — only the first of each email+subject will be sent',
|
||||||
|
'system.batchEmail.dupConfirmHint': 'Duplicates are skipped by default. Click “Send anyway” to include them.',
|
||||||
|
'system.batchEmail.dupConfirmForce': 'Send anyway',
|
||||||
|
'system.batchEmail.dupConfirmSkip': 'Skip duplicates',
|
||||||
|
'system.batchEmail.failDupRecent': 'Already sent with the same subject within {days} days',
|
||||||
|
'system.batchEmail.failDupInBatch': 'Duplicate in this batch, skipped',
|
||||||
'system.batchEmail.warnEmptyContent': 'Template body is required',
|
'system.batchEmail.warnEmptyContent': 'Template body is required',
|
||||||
'system.batchEmail.warnEmptyName': 'Template name is required',
|
'system.batchEmail.warnEmptyName': 'Template name is required',
|
||||||
'system.batchEmail.confirmOverwriteTitle': 'Overwrite?',
|
'system.batchEmail.confirmOverwriteTitle': 'Overwrite?',
|
||||||
|
|||||||
@ -4423,11 +4423,24 @@ export default {
|
|||||||
'system.batchEmail.errLoadTemplates': '加载模板失败',
|
'system.batchEmail.errLoadTemplates': '加载模板失败',
|
||||||
'system.batchEmail.errLoadRecords': '加载邮件记录失败',
|
'system.batchEmail.errLoadRecords': '加载邮件记录失败',
|
||||||
'system.batchEmail.recordList': '邮件记录',
|
'system.batchEmail.recordList': '邮件记录',
|
||||||
|
'system.batchEmail.recordSearch': '查询',
|
||||||
|
'system.batchEmail.recordSearchPlaceholder': '邮箱 / 主题 / 内容 / 发送人',
|
||||||
'system.batchEmail.record.toEmail': '邮箱',
|
'system.batchEmail.record.toEmail': '邮箱',
|
||||||
'system.batchEmail.record.subject': '主题',
|
'system.batchEmail.record.subject': '主题',
|
||||||
'system.batchEmail.record.content': '内容',
|
'system.batchEmail.record.content': '内容',
|
||||||
'system.batchEmail.record.sendTime': '发送时间',
|
'system.batchEmail.record.sendTime': '发送时间',
|
||||||
'system.batchEmail.record.sender': '发送人',
|
'system.batchEmail.record.sender': '发送人',
|
||||||
|
'system.batchEmail.errCheckDup': '查重失败,请稍后重试',
|
||||||
|
'system.batchEmail.dupConfirmTitle': '发现近 30 天重复发送',
|
||||||
|
'system.batchEmail.dupConfirmBody': '有 {count} 条「同邮箱 + 同主题」在近 {days} 天内已发送过(含本批重复):',
|
||||||
|
'system.batchEmail.dupConfirmMore': '另有 {n} 条未展示',
|
||||||
|
'system.batchEmail.dupConfirmBatchOnly': '另有 {n} 条为本批内重复收件',
|
||||||
|
'system.batchEmail.dupBatchOnlyTip': '本批有 {n} 组重复收件,同邮箱同主题仅发送首条',
|
||||||
|
'system.batchEmail.dupConfirmHint': '默认跳过重复项;若仍要发送,请点「仍要发送」。',
|
||||||
|
'system.batchEmail.dupConfirmForce': '仍要发送',
|
||||||
|
'system.batchEmail.dupConfirmSkip': '仅发送未重复',
|
||||||
|
'system.batchEmail.failDupRecent': '近 {days} 天已发送过同主题',
|
||||||
|
'system.batchEmail.failDupInBatch': '本批内重复,已跳过',
|
||||||
'system.batchEmail.warnEmptyContent': '模板内容不能为空',
|
'system.batchEmail.warnEmptyContent': '模板内容不能为空',
|
||||||
'system.batchEmail.warnEmptyName': '模板名称不能为空',
|
'system.batchEmail.warnEmptyName': '模板名称不能为空',
|
||||||
'system.batchEmail.confirmOverwriteTitle': '确认覆盖',
|
'system.batchEmail.confirmOverwriteTitle': '确认覆盖',
|
||||||
|
|||||||
@ -325,6 +325,8 @@ void main() {
|
|||||||
canvas.style.height = '100%';
|
canvas.style.height = '100%';
|
||||||
canvas.style.display = 'block';
|
canvas.style.display = 'block';
|
||||||
canvas.style.pointerEvents = 'none';
|
canvas.style.pointerEvents = 'none';
|
||||||
|
canvas.style.opacity = '0';
|
||||||
|
canvas.style.transition = 'opacity 0.55s ease';
|
||||||
container.appendChild(canvas);
|
container.appendChild(canvas);
|
||||||
|
|
||||||
const geometry = new Triangle(gl);
|
const geometry = new Triangle(gl);
|
||||||
@ -457,6 +459,11 @@ void main() {
|
|||||||
|
|
||||||
tryStart();
|
tryStart();
|
||||||
syncUniforms();
|
syncUniforms();
|
||||||
|
// 先画一帧再淡入,盖在静态海天底上,避免灰/白闪
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
paint(performance.now());
|
||||||
|
canvas.style.opacity = '1';
|
||||||
|
});
|
||||||
|
|
||||||
teardown = () => {
|
teardown = () => {
|
||||||
tryStop();
|
tryStop();
|
||||||
@ -494,5 +501,13 @@ void main() {
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
|
will-change: opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.gradient-waves :deep(canvas) {
|
||||||
|
transition: none !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -40,7 +40,8 @@
|
|||||||
as="span"
|
as="span"
|
||||||
class-name="ai-welcome-title__type"
|
class-name="ai-welcome-title__type"
|
||||||
:text="shortTitle ? copy.titleRestShort : copy.titleRest"
|
:text="shortTitle ? copy.titleRestShort : copy.titleRest"
|
||||||
:typing-speed="36"
|
:typing-speed="48"
|
||||||
|
:initial-delay="280"
|
||||||
:pause-duration="900"
|
:pause-duration="900"
|
||||||
:show-cursor="false"
|
:show-cursor="false"
|
||||||
:loop="false"
|
:loop="false"
|
||||||
|
|||||||
@ -133,7 +133,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import SiteHeader from './SiteHeader.vue';
|
import SiteHeader from './SiteHeader.vue';
|
||||||
import SiteAdvisorShell from './SiteAdvisorShell.vue';
|
import SiteAdvisorShell from './SiteAdvisorShell.vue';
|
||||||
@ -141,6 +141,7 @@
|
|||||||
import SiteServicesPanel from './SiteServicesPanel.vue';
|
import SiteServicesPanel from './SiteServicesPanel.vue';
|
||||||
import SiteTopicPanel from './SiteTopicPanel.vue';
|
import SiteTopicPanel from './SiteTopicPanel.vue';
|
||||||
import SiteWecomModal from './SiteWecomModal.vue';
|
import SiteWecomModal from './SiteWecomModal.vue';
|
||||||
|
import GradientWaves from './GradientWaves.vue';
|
||||||
import {
|
import {
|
||||||
goOfficialLoginFromHref,
|
goOfficialLoginFromHref,
|
||||||
officialLoginUrl,
|
officialLoginUrl,
|
||||||
@ -154,7 +155,6 @@
|
|||||||
import './styles/main.css';
|
import './styles/main.css';
|
||||||
import './styles/ai-advisor.css';
|
import './styles/ai-advisor.css';
|
||||||
|
|
||||||
const GradientWaves = defineAsyncComponent(() => import('./GradientWaves.vue'));
|
|
||||||
const glass = provideSiteLiquidGlass();
|
const glass = provideSiteLiquidGlass();
|
||||||
const glassSceneEl = ref<HTMLElement | null>(null);
|
const glassSceneEl = ref<HTMLElement | null>(null);
|
||||||
const glassContentEl = ref<HTMLElement | null>(null);
|
const glassContentEl = ref<HTMLElement | null>(null);
|
||||||
|
|||||||
@ -2526,24 +2526,56 @@ body.command-home .ai-stage-map--eu {
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0 0 16px;
|
padding: 0 0 16px;
|
||||||
animation: ai-h5-rise-in 0.42s var(--ai-ease) both;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-welcome.is-hidden {
|
.ai-welcome.is-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes ai-h5-rise-in {
|
/* 首页入场:轻上移 + 淡入,分拍错开,避免整块突然砸下来 */
|
||||||
|
@keyframes ai-welcome-rise {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(10px);
|
transform: translate3d(0, 14px, 0);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translate3d(0, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-welcome__intro,
|
||||||
|
.ai-welcome__meta,
|
||||||
|
.ai-welcome-compose-panel,
|
||||||
|
.ai-intent-group,
|
||||||
|
.ai-trust-bar {
|
||||||
|
animation: ai-welcome-rise 0.78s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-welcome__intro {
|
||||||
|
animation-delay: 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-welcome__meta {
|
||||||
|
animation-delay: 0.26s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-welcome-compose-panel {
|
||||||
|
animation-delay: 0.38s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-intent-group:nth-child(1) {
|
||||||
|
animation-delay: 0.52s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-intent-group:nth-child(2) {
|
||||||
|
animation-delay: 0.66s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-trust-bar {
|
||||||
|
animation-delay: 0.8s;
|
||||||
|
}
|
||||||
|
|
||||||
.ai-welcome__hero {
|
.ai-welcome__hero {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -2813,10 +2845,6 @@ body.command-home #navbar .site-nav-avatar:hover {
|
|||||||
height: auto;
|
height: auto;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
background: color-mix(in srgb, #fff 70%, #e8eef2);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.75),
|
|
||||||
0 1px 2px rgba(15, 23, 42, 0.03);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-welcome__command .ai-intent-grid--growth .ai-intent-door__head {
|
.ai-welcome__command .ai-intent-grid--growth .ai-intent-door__head {
|
||||||
@ -2856,20 +2884,19 @@ body.command-home #navbar .site-nav-avatar:hover {
|
|||||||
line-clamp: 2;
|
line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 履约 / 权益门卡(liquid-glass 表面) */
|
/* 履约 / 权益门卡(liquid-glass 表面)
|
||||||
|
未就绪也用半透明玻璃底,避免「先白再玻璃」闪一下;实心霜面仅 fallback */
|
||||||
.ai-welcome__command .ai-intent-door {
|
.ai-welcome__command .ai-intent-door {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
border: 1px solid rgba(255, 255, 255, 0.42);
|
||||||
border-radius: var(--ai-dock-radius);
|
border-radius: var(--ai-dock-radius);
|
||||||
padding: 14px 14px;
|
padding: 14px 14px;
|
||||||
background: color-mix(in srgb, #fff 78%, #e6edf2);
|
background: rgba(255, 255, 255, 0.16);
|
||||||
backdrop-filter: blur(18px) saturate(150%);
|
backdrop-filter: none;
|
||||||
-webkit-backdrop-filter: blur(18px) saturate(150%);
|
-webkit-backdrop-filter: none;
|
||||||
box-shadow:
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.38);
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.8),
|
|
||||||
var(--ai-paper-shadow);
|
|
||||||
transition:
|
transition:
|
||||||
background 0.2s var(--ai-ease),
|
border-color 0.2s var(--ai-ease),
|
||||||
box-shadow 0.2s var(--ai-ease),
|
box-shadow 0.2s var(--ai-ease),
|
||||||
transform 0.2s var(--ai-ease);
|
transform 0.2s var(--ai-ease);
|
||||||
--lg-foreground: var(--ai-brand, #023e8a);
|
--lg-foreground: var(--ai-brand, #023e8a);
|
||||||
@ -2904,11 +2931,9 @@ body.command-home #navbar .site-nav-avatar:hover {
|
|||||||
position: relative;
|
position: relative;
|
||||||
padding: 16px 16px;
|
padding: 16px 16px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: color-mix(in srgb, #fff 62%, #e8eef3);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
border: 1px solid rgba(255, 255, 255, 0.48);
|
||||||
box-shadow:
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.42);
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
|
||||||
0 2px 8px rgba(15, 23, 42, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-welcome__command .ai-intent-door.is-primary .ai-intent-door__icon {
|
.ai-welcome__command .ai-intent-door.is-primary .ai-intent-door__icon {
|
||||||
@ -2925,29 +2950,25 @@ body.command-home #navbar .site-nav-avatar:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ai-welcome__command .ai-intent-door:hover {
|
.ai-welcome__command .ai-intent-door:hover {
|
||||||
border-color: rgba(255, 255, 255, 0.9);
|
border-color: rgba(255, 255, 255, 0.62);
|
||||||
background: color-mix(in srgb, #fff 78%, #e8eef3);
|
background: rgba(255, 255, 255, 0.22);
|
||||||
box-shadow:
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.95),
|
|
||||||
0 6px 18px rgba(15, 23, 42, 0.07);
|
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-welcome__command .ai-intent-door.is-primary:hover {
|
.ai-welcome__command .ai-intent-door.is-primary:hover {
|
||||||
border-color: rgba(255, 255, 255, 0.95);
|
border-color: rgba(255, 255, 255, 0.68);
|
||||||
box-shadow:
|
background: rgba(255, 255, 255, 0.26);
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 1),
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55);
|
||||||
0 8px 22px rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* liquid-glass 就绪后透出折射;未就绪 / 降级时保留 CSS 霜面 */
|
/* liquid-glass 就绪后透出折射 */
|
||||||
.ai-welcome__command .ai-intent-door.lg-surface.is-glass-ready:not(.is-glass-fallback),
|
.ai-welcome__command .ai-intent-door.lg-surface.is-glass-ready:not(.is-glass-fallback),
|
||||||
.ai-welcome__command .ai-intent-door.lg-surface.is-glass-ready:not(.is-glass-fallback).is-primary {
|
.ai-welcome__command .ai-intent-door.lg-surface.is-glass-ready:not(.is-glass-fallback).is-primary,
|
||||||
|
.ai-welcome__command .ai-intent-grid--growth .ai-intent-door.lg-surface.is-glass-ready:not(.is-glass-fallback) {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
backdrop-filter: none;
|
|
||||||
-webkit-backdrop-filter: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-welcome__command .ai-intent-door.lg-surface:not(.is-glass-ready),
|
.ai-welcome__command .ai-intent-door.lg-surface:not(.is-glass-ready),
|
||||||
@ -2955,9 +2976,11 @@ body.command-home #navbar .site-nav-avatar:hover {
|
|||||||
visibility: visible !important;
|
visibility: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-welcome__command .ai-intent-grid--growth .ai-intent-door.lg-surface:not(.is-glass-ready),
|
/* 仅 WebGPU 不可用时才用实心霜面,避免首屏白闪 */
|
||||||
|
.ai-welcome__command .ai-intent-door.lg-surface.is-glass-fallback,
|
||||||
|
.ai-welcome__command .ai-intent-door.lg-surface.is-glass-fallback.is-primary,
|
||||||
.ai-welcome__command .ai-intent-grid--growth .ai-intent-door.lg-surface.is-glass-fallback {
|
.ai-welcome__command .ai-intent-grid--growth .ai-intent-door.lg-surface.is-glass-fallback {
|
||||||
background: color-mix(in srgb, #fff 70%, #e8eef2);
|
background: color-mix(in srgb, #fff 72%, #e8eef2);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.75),
|
inset 0 1px 0 rgba(255, 255, 255, 0.75),
|
||||||
@ -3040,23 +3063,28 @@ body.command-home #navbar .site-nav-avatar:hover {
|
|||||||
display: block;
|
display: block;
|
||||||
filter: drop-shadow(0 12px 24px rgba(var(--ai-sky-rgb), 0.1));
|
filter: drop-shadow(0 12px 24px rgba(var(--ai-sky-rgb), 0.1));
|
||||||
transform-origin: 50% 60%;
|
transform-origin: 50% 60%;
|
||||||
animation: aiPlaneIn 1.8s cubic-bezier(0.22, 1, 0.36, 1) both;
|
animation: aiPlaneIn 1.15s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes aiPlaneIn {
|
@keyframes aiPlaneIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translate3d(-10%, 18%, 0) scale(0.72) rotate(-3deg);
|
transform: translate3d(0, 16px, 0) scale(0.96);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translate3d(0, 0, 0) scale(1) rotate(0deg);
|
transform: translate3d(0, 0, 0) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.ai-welcome-plane img {
|
.ai-welcome-plane img,
|
||||||
animation: none;
|
.ai-welcome__intro,
|
||||||
|
.ai-welcome__meta,
|
||||||
|
.ai-welcome-compose-panel,
|
||||||
|
.ai-intent-group,
|
||||||
|
.ai-trust-bar {
|
||||||
|
animation: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5216,13 +5244,14 @@ body.command-home * {
|
|||||||
-webkit-backdrop-filter: none !important;
|
-webkit-backdrop-filter: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep specular sheen; drop animated glass blobs only */
|
/* Keep specular sheen off welcome glass doors (sheen reads as white flash before LG ready) */
|
||||||
body.command-home .ai-intent-door::after {
|
body.command-home .ai-intent-door::after,
|
||||||
|
body.command-home .ai-welcome__command .ai-intent-door::before,
|
||||||
|
body.command-home .ai-welcome__command .ai-intent-door::after {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.command-home .ai-intent-door::before,
|
body.command-home .ai-intent-door:not(.lg-surface)::before {
|
||||||
body.command-home .ai-welcome__command .ai-intent-door::before {
|
|
||||||
display: block !important;
|
display: block !important;
|
||||||
background: var(--ai-glass-sheen);
|
background: var(--ai-glass-sheen);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -201,8 +201,24 @@
|
|||||||
</span>
|
</span>
|
||||||
<a-tag size="small" color="gray">{{ recordTotal }}</a-tag>
|
<a-tag size="small" color="gray">{{ recordTotal }}</a-tag>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-show="recordsExpanded" class="record-body">
|
||||||
|
<div class="table-toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<a-input
|
||||||
|
v-model="recordKeyword"
|
||||||
|
allow-clear
|
||||||
|
:placeholder="t('system.batchEmail.recordSearchPlaceholder')"
|
||||||
|
style="width: 260px"
|
||||||
|
@press-enter="handleRecordSearch"
|
||||||
|
@clear="handleRecordSearch"
|
||||||
|
/>
|
||||||
|
<a-button class="app-btn-search" @click="handleRecordSearch">
|
||||||
|
{{ t('system.batchEmail.recordSearch') }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">
|
||||||
<PaginationPro
|
<PaginationPro
|
||||||
v-show="recordsExpanded"
|
|
||||||
:total-count="recordTotal"
|
:total-count="recordTotal"
|
||||||
v-model:current-page="recordPage"
|
v-model:current-page="recordPage"
|
||||||
v-model:page-size="recordPageSize"
|
v-model:page-size="recordPageSize"
|
||||||
@ -211,23 +227,22 @@
|
|||||||
@page-size-change="handleRecordPageSizeChange"
|
@page-size-change="handleRecordPageSizeChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="recordsExpanded" class="list-wrap">
|
</div>
|
||||||
<a-table
|
<a-spin :loading="recordLoading" class="record-spin">
|
||||||
:columns="recordColumns"
|
<div ref="recordTableContainerRef" class="list-wrap">
|
||||||
|
<HandsontablePro
|
||||||
|
:columns="recordHotColumns"
|
||||||
:data="recordList"
|
:data="recordList"
|
||||||
:loading="recordLoading"
|
:total="recordTotal"
|
||||||
:pagination="false"
|
:current-page="recordPage"
|
||||||
:scroll="{ y: '100%' }"
|
:page-size="recordPageSize"
|
||||||
row-key="id"
|
:stretch-columns="true"
|
||||||
size="small"
|
:height="recordHandsontableHeight"
|
||||||
class="record-table"
|
:read-only="true"
|
||||||
>
|
:show-action-column="false"
|
||||||
<template #content="{ record }">
|
/>
|
||||||
<a-tooltip :content="record.content">
|
</div>
|
||||||
<span class="record-content-cell">{{ record.content }}</span>
|
</a-spin>
|
||||||
</a-tooltip>
|
|
||||||
</template>
|
|
||||||
</a-table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -388,15 +403,18 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({ name: 'SystemBatchEmail' })
|
defineOptions({ name: 'SystemBatchEmail' })
|
||||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
import { computed, h, nextTick, onMounted, reactive, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import * as XLSX from 'xlsx'
|
import * as XLSX from 'xlsx'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Message, Modal } from '@arco-design/web-vue'
|
import { Message, Modal } from '@arco-design/web-vue'
|
||||||
import { IconDown } from '@arco-design/web-vue/es/icon'
|
import { IconDown } from '@arco-design/web-vue/es/icon'
|
||||||
|
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||||
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||||
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||||
import {
|
import {
|
||||||
|
checkBatchEmailDuplicates,
|
||||||
deleteBatchEmailTemplate,
|
deleteBatchEmailTemplate,
|
||||||
getBatchEmailAccount,
|
getBatchEmailAccount,
|
||||||
isSmtpSecretMask,
|
isSmtpSecretMask,
|
||||||
@ -407,6 +425,7 @@ import {
|
|||||||
sendBatchEmailOne,
|
sendBatchEmailOne,
|
||||||
SMTP_SECRET_MASK,
|
SMTP_SECRET_MASK,
|
||||||
testBatchEmailConnection,
|
testBatchEmailConnection,
|
||||||
|
type BatchEmailDuplicateHit,
|
||||||
type BatchEmailSendRecord,
|
type BatchEmailSendRecord,
|
||||||
type BatchEmailServerType,
|
type BatchEmailServerType,
|
||||||
} from '@/api/system/batchEmail'
|
} from '@/api/system/batchEmail'
|
||||||
@ -494,10 +513,16 @@ const saveTplLoading = ref(false)
|
|||||||
const recordList = ref<BatchEmailSendRecord[]>([])
|
const recordList = ref<BatchEmailSendRecord[]>([])
|
||||||
const recordTotal = ref(0)
|
const recordTotal = ref(0)
|
||||||
const recordPage = ref(1)
|
const recordPage = ref(1)
|
||||||
const recordPageSize = ref(30)
|
const recordPageSize = ref(50)
|
||||||
|
const recordKeyword = ref('')
|
||||||
const recordLoading = ref(false)
|
const recordLoading = ref(false)
|
||||||
|
const { tableContainerRef: recordTableContainerRef, tableHeight: recordTableHeight } =
|
||||||
|
useTableContainerHeight()
|
||||||
|
const recordHandsontableHeight = computed(() => Math.max(recordTableHeight.value || 0, 280))
|
||||||
const recipientPreviewLimit = 8
|
const recipientPreviewLimit = 8
|
||||||
const SEND_CONCURRENCY = 3
|
const SEND_CONCURRENCY = 3
|
||||||
|
const DUP_WITHIN_DAYS = 30
|
||||||
|
const DUP_PREVIEW_LIMIT = 8
|
||||||
|
|
||||||
const sendConfirmVisible = ref(false)
|
const sendConfirmVisible = ref(false)
|
||||||
const sendAbortRequested = ref(false)
|
const sendAbortRequested = ref(false)
|
||||||
@ -596,14 +621,18 @@ const recipientPreviewRows = computed(() =>
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
||||||
const recordColumns = computed(() => [
|
const recordHotColumns = computed(() => [
|
||||||
{ title: t('system.batchEmail.record.toEmail'), dataIndex: 'toEmail', width: 160, ellipsis: true, tooltip: true },
|
{ key: 'toEmail', label: t('system.batchEmail.record.toEmail'), width: 180 },
|
||||||
{ title: t('system.batchEmail.record.subject'), dataIndex: 'subject', width: 140, ellipsis: true, tooltip: true },
|
{ key: 'subject', label: t('system.batchEmail.record.subject'), width: 200 },
|
||||||
{ title: t('system.batchEmail.record.content'), dataIndex: 'content', slotName: 'content', ellipsis: true },
|
{ key: 'content', label: t('system.batchEmail.record.content'), width: 320 },
|
||||||
{ title: t('system.batchEmail.record.sendTime'), dataIndex: 'sendTime', width: 160 },
|
{ key: 'sendTime', label: t('system.batchEmail.record.sendTime'), width: 170 },
|
||||||
{ title: t('system.batchEmail.record.sender'), dataIndex: 'sender', width: 100, ellipsis: true, tooltip: true },
|
{ key: 'sender', label: t('system.batchEmail.record.sender'), width: 120 },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
function buildDuplicateKey(email: string, subject: string) {
|
||||||
|
return `${email.trim().toLowerCase()}\u0001${subject.trim()}`
|
||||||
|
}
|
||||||
|
|
||||||
function templatesToMap(list: Array<{ name: string; subject: string; content: string }>): Record<string, TemplateData> {
|
function templatesToMap(list: Array<{ name: string; subject: string; content: string }>): Record<string, TemplateData> {
|
||||||
const out: Record<string, TemplateData> = {}
|
const out: Record<string, TemplateData> = {}
|
||||||
list.forEach((item) => {
|
list.forEach((item) => {
|
||||||
@ -768,6 +797,7 @@ async function loadRecords() {
|
|||||||
const { items, total } = await listBatchEmailRecords({
|
const { items, total } = await listBatchEmailRecords({
|
||||||
page: recordPage.value,
|
page: recordPage.value,
|
||||||
pageSize: recordPageSize.value,
|
pageSize: recordPageSize.value,
|
||||||
|
keyword: recordKeyword.value.trim() || undefined,
|
||||||
})
|
})
|
||||||
recordList.value = items.map((item) => ({
|
recordList.value = items.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
@ -782,6 +812,11 @@ async function loadRecords() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRecordSearch() {
|
||||||
|
recordPage.value = 1
|
||||||
|
void loadRecords()
|
||||||
|
}
|
||||||
|
|
||||||
function handleRecordPageSizeChange(size: number) {
|
function handleRecordPageSizeChange(size: number) {
|
||||||
recordPageSize.value = size
|
recordPageSize.value = size
|
||||||
recordPage.value = 1
|
recordPage.value = 1
|
||||||
@ -1114,7 +1149,108 @@ function openSendConfirm() {
|
|||||||
|
|
||||||
function confirmAndStartSend() {
|
function confirmAndStartSend() {
|
||||||
sendConfirmVisible.value = false
|
sendConfirmVisible.value = false
|
||||||
|
void prepareAndStartSend()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDupHitLine(hit: BatchEmailDuplicateHit) {
|
||||||
|
const time = formatSendTime(hit.sendTime)
|
||||||
|
return `${hit.toEmail} · ${hit.subject || '-'} · ${time}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareAndStartSend() {
|
||||||
|
const recipients = getRecipientsForSend()
|
||||||
|
const subjBase =
|
||||||
|
subjectDraft.value.trim() || selectedTemplateName.value || t('system.batchEmail.fallbackSubject')
|
||||||
|
|
||||||
|
const pairs: Array<{ to: string; subject: string; key: string }> = []
|
||||||
|
const seenInBatch = new Set<string>()
|
||||||
|
const batchDupKeys = new Set<string>()
|
||||||
|
|
||||||
|
for (const order of recipients) {
|
||||||
|
const email = order.寄件人邮箱?.trim() ?? ''
|
||||||
|
if (!email || !email.includes('@')) continue
|
||||||
|
const subject = fillPlaceholders(subjBase, order).trim()
|
||||||
|
const key = buildDuplicateKey(email, subject)
|
||||||
|
if (seenInBatch.has(key)) {
|
||||||
|
batchDupKeys.add(key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenInBatch.add(key)
|
||||||
|
pairs.push({ to: email, subject, key })
|
||||||
|
}
|
||||||
|
|
||||||
|
let historyHits: BatchEmailDuplicateHit[] = []
|
||||||
|
try {
|
||||||
|
if (pairs.length) {
|
||||||
|
historyHits = await checkBatchEmailDuplicates({
|
||||||
|
pairs: pairs.map((p) => ({ to: p.to, subject: p.subject })),
|
||||||
|
withinDays: DUP_WITHIN_DAYS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
Message.error((e as Error)?.message || t('system.batchEmail.errCheckDup'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const historyKeys = new Set(
|
||||||
|
historyHits.map((h) => buildDuplicateKey(h.toEmail, h.subject)),
|
||||||
|
)
|
||||||
|
const dupCount = historyKeys.size + batchDupKeys.size
|
||||||
|
|
||||||
|
if (dupCount === 0) {
|
||||||
void runBatchSend()
|
void runBatchSend()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅历史命中才弹软拦;本批重复始终靠 sentInBatchKeys 只发首条
|
||||||
|
if (historyKeys.size === 0) {
|
||||||
|
if (batchDupKeys.size > 0) {
|
||||||
|
Message.info(t('system.batchEmail.dupBatchOnlyTip', { n: batchDupKeys.size }))
|
||||||
|
}
|
||||||
|
void runBatchSend()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewLines = historyHits.slice(0, DUP_PREVIEW_LIMIT).map(formatDupHitLine)
|
||||||
|
const more = Math.max(0, historyHits.length - DUP_PREVIEW_LIMIT)
|
||||||
|
const batchOnlyCount = [...batchDupKeys].filter((k) => !historyKeys.has(k)).length
|
||||||
|
const contentParts = [
|
||||||
|
t('system.batchEmail.dupConfirmBody', {
|
||||||
|
days: DUP_WITHIN_DAYS,
|
||||||
|
count: historyKeys.size,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
if (previewLines.length) {
|
||||||
|
contentParts.push(previewLines.join('\n'))
|
||||||
|
if (more > 0) {
|
||||||
|
contentParts.push(t('system.batchEmail.dupConfirmMore', { n: more }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (batchOnlyCount > 0) {
|
||||||
|
contentParts.push(t('system.batchEmail.dupConfirmBatchOnly', { n: batchOnlyCount }))
|
||||||
|
}
|
||||||
|
contentParts.push(t('system.batchEmail.dupConfirmHint'))
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('system.batchEmail.dupConfirmTitle'),
|
||||||
|
content: () =>
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ style: 'white-space: pre-wrap; line-height: 1.6; max-height: 360px; overflow: auto;' },
|
||||||
|
contentParts.join('\n\n'),
|
||||||
|
),
|
||||||
|
okText: t('system.batchEmail.dupConfirmForce'),
|
||||||
|
cancelText: t('system.batchEmail.dupConfirmSkip'),
|
||||||
|
hideCancel: false,
|
||||||
|
simple: false,
|
||||||
|
width: 560,
|
||||||
|
onOk: () => {
|
||||||
|
void runBatchSend()
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
void runBatchSend({ skipDuplicateKeys: historyKeys })
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelBatchSend() {
|
function cancelBatchSend() {
|
||||||
@ -1165,13 +1301,15 @@ async function runWithConcurrency<T>(
|
|||||||
await Promise.all(runners)
|
await Promise.all(runners)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runBatchSend() {
|
async function runBatchSend(options?: { skipDuplicateKeys?: Set<string> }) {
|
||||||
const recipients = getRecipientsForSend()
|
const recipients = getRecipientsForSend()
|
||||||
const bodyContent = contentDraft.value.trim()
|
const bodyContent = contentDraft.value.trim()
|
||||||
const u = accountUsername.value.trim()
|
const u = accountUsername.value.trim()
|
||||||
const p = resolvePasswordForRequest()
|
const p = resolvePasswordForRequest()
|
||||||
const subjBase =
|
const subjBase =
|
||||||
subjectDraft.value.trim() || selectedTemplateName.value || t('system.batchEmail.fallbackSubject')
|
subjectDraft.value.trim() || selectedTemplateName.value || t('system.batchEmail.fallbackSubject')
|
||||||
|
const skipKeys = options?.skipDuplicateKeys
|
||||||
|
const sentInBatchKeys = new Set<string>()
|
||||||
|
|
||||||
sending.value = true
|
sending.value = true
|
||||||
sendAbortRequested.value = false
|
sendAbortRequested.value = false
|
||||||
@ -1200,6 +1338,20 @@ async function runBatchSend() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const subject = fillPlaceholders(subjBase, order)
|
const subject = fillPlaceholders(subjBase, order)
|
||||||
|
const dupKey = buildDuplicateKey(email, subject)
|
||||||
|
|
||||||
|
if (skipKeys?.has(dupKey)) {
|
||||||
|
pushSendFailure(email, t('system.batchEmail.failDupRecent', { days: DUP_WITHIN_DAYS }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 同批第二次起不连发(先占位,避免并发双发)
|
||||||
|
if (sentInBatchKeys.has(dupKey)) {
|
||||||
|
pushSendFailure(email, t('system.batchEmail.failDupInBatch'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sentInBatchKeys.add(dupKey)
|
||||||
|
|
||||||
|
try {
|
||||||
await sendBatchEmailOne({
|
await sendBatchEmailOne({
|
||||||
smtpUsername: u,
|
smtpUsername: u,
|
||||||
smtpPassword: p,
|
smtpPassword: p,
|
||||||
@ -1210,6 +1362,10 @@ async function runBatchSend() {
|
|||||||
bodyType: 'plain',
|
bodyType: 'plain',
|
||||||
})
|
})
|
||||||
sendProgress.success += 1
|
sendProgress.success += 1
|
||||||
|
} catch (sendErr: unknown) {
|
||||||
|
sentInBatchKeys.delete(dupKey)
|
||||||
|
throw sendErr
|
||||||
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
pushSendFailure(email, extractErrorMessage(e))
|
pushSendFailure(email, extractErrorMessage(e))
|
||||||
} finally {
|
} finally {
|
||||||
@ -1640,7 +1796,6 @@ onMounted(async () => {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-bottom: 1px solid var(--color-border-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-toggle {
|
.record-toggle {
|
||||||
@ -1662,30 +1817,48 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-wrap {
|
.record-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 12px 16px 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 0 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-table {
|
.record-body .table-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-body .toolbar-left,
|
||||||
|
.record-body .toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-spin {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
:deep(.arco-table),
|
:deep(.arco-spin-container) {
|
||||||
:deep(.arco-table-container) {
|
flex: 1;
|
||||||
height: 100%;
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-content-cell {
|
.list-wrap {
|
||||||
display: inline-block;
|
flex: 1;
|
||||||
max-width: 100%;
|
min-height: 0;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-input-hidden {
|
.file-input-hidden {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user