Commit b09264c7 by Sweet Zhang

第一次构建

parent 4de2993b
VITE_API_BASE_URL=/email/api
\ No newline at end of file
VITE_API_BASE_URL='http://139.224.145.34:9002/email/api'
\ No newline at end of file
interface resPage {
page: number
pageSize: number
total: number
hasNextPage: boolean
pageTotal: number
}
interface itfRes {
code: number
msg: string | undefined
data: T
reason?: string
page?: resPage
}
import * as axios from 'axios'
declare module 'axios'
{interface AxiosResponse extends itfRes}
......@@ -8,7 +8,7 @@
},
"scripts": {
"dev": "vite --mode development",
"build": "run-p type-check \"build-only {@}\" --",
"build": "run-p build-only",
"preview": "vite preview",
"test:unit": "vitest",
"test:e2e": "playwright test",
......
......@@ -29,7 +29,7 @@
<main class="flex-1 overflow-y-auto bg-gray-50 p-4 md:p-6">
<header class="mb-6">
<h2 class="text-2xl font-bold text-gray-800">
{{ pageTitles[currentPage] }}
<!-- {{ pageTitles[currentPage] }} -->
</h2>
</header>
......
import request from '@/utils/request'
import type { EditContactImport } from '@/types/index'
// 新增联系人
/**
......@@ -24,7 +25,16 @@ export const addContact = (data: {
* @param data {"contactBizId":1,"companyName":"","name":"","email":"","type":"","appellation":"","other":"","ccEmailList":[]}
* @returns
*/
export const editContact = (data) => {
export const editContact = (data: {
contactBizId?: string
companyName?: string
name?: string
email?: string
type?: string
appellation?: string
other?: string
ccEmailList?: string[]
}) => {
return request.put('/emailContact/edit', data)
}
......@@ -96,30 +106,30 @@ export const getEmailProviderList = (params: {
return request.post('/emailProviderConfig/page', params)
}
// 新增发送配置
// 新增发送配置
/**
*
* @param data {"emailSenderConfigBizId":1,"emailSenderConfigName":"","emailSenderConfigEmail":"","emailSenderConfigType":"","emailSenderConfigAppellation":"","emailSenderConfigOther":"","emailSenderConfigCcEmailList":[]}
* @returns
*/
export const addEmailSenderConfig = (data: {
email?: number
email?: string
password?: string
providerBizId?: string
displayName?: string
active?: boolean
active?: number
}) => {
return request.post('/emailSenderConfig/add', data)
}
// 编辑发送配置
// 编辑发送配置
/**
*
* @param data {"emailSenderConfigBizId":1,"emailSenderConfigName":"","emailSenderConfigEmail":"","emailSenderConfigType":"","emailSenderConfigAppellation":"","emailSenderConfigOther":"","emailSenderConfigCcEmailList":[]}
* @returns
*/
export const editEmailSenderConfig = (data: {
senderBizId?: number
senderBizId?: string
email?: string
password?: string
providerBizId?: string
......@@ -129,7 +139,7 @@ export const editEmailSenderConfig = (data: {
return request.put('/emailSenderConfig/edit', data)
}
// 删除发送配置
// 删除发送配置
/**
*
* @param id 发送配置id
......@@ -139,7 +149,7 @@ export const deleteEmailSenderConfig = (id: string) => {
return request.delete('/emailSenderConfig/del?senderBizId=' + id)
}
// 获取发送配置详情
// 获取发送配置详情
/**
*
* @param id 发送配置id
......@@ -322,3 +332,51 @@ export const getEmailVariableGroupList = (params: {
export const getEmailVariableGroupDetail = (id: string) => {
return request.get('/emailVariableGroup/detail?variableGroupBizId=' + id)
}
/**
* 选择联系人时,调用接口,获取sessionId
*/
/**
*
* @returns
*/
export const getEmailContactSessionId = (params) => {
return request.post('/emailContactImport/select/add', params)
}
/**
* 编辑-邮件联系人导入信息
*/
/**
*
* @param data {"sessionId": "", //会话ID
"apiEmailContactDtoList": [
{
"email": "", //邮箱
"name": "" //姓名
}
] //联系人列表
}
* @returns
*/
export const editEmailContactImport = (data: EditContactImport) => {
return request.put('/emailContactImport/edit', data)
}
/**
* 邮件联系人导入列表查询
*/
/**
*
* @param params {
"sessionId": "", //会话ID
"pageNo": 1,
"pageSize": 1,
"sortField": "",
"sortOrder": ""
}
* @returns
*/
export const getEmailContactImportList = (params: EditContactImport) => {
return request.post('/emailContactImport/page', params)
}
import * as axios from 'axios'
declare module 'axios' {
interface AxiosInstance {
(config: AxiosRequestConfig): Promise<any>
}
}
......@@ -204,6 +204,7 @@ const emit = defineEmits<{
// 内部状态管理
const dialogVisible = ref(props.visible)
// 定时器
const autoCloseTimer = ref<NodeJS.Timeout | null>(null)
// 监听visible变化
......
......@@ -40,14 +40,14 @@ export interface VariableTemplate {
// 邮件类型
export interface Email {
id: string
sender: string
to: string
cc: string
subject: string
content: string
sendTime: string
status: 'sent' | 'scheduled' | 'draft' | 'failed'
id?: string
sender?: string
to?: string[]
cc?: string[]
subject?: string
content?: string
sendTime?: string
status?: 'sent' | 'scheduled' | 'draft' | 'failed'
attachments?: { name: string }[]
}
......@@ -76,3 +76,20 @@ export interface ImportRecord {
createdAt: string
updatedAt: string
}
// 选择联系人时,调用接口,获取sessionId
export interface ContactSessionId {
sessionId?: string
apiEmailContactDtoList?: Contact[]
}
// 编辑-邮件联系人导入信息
export interface EditContactImport {
receiveEmail?: string
sessionId?: string
source?: string
pageNo?: number
pageSize?: number
sortField?: string
sortOrder?: string
}
......@@ -2,7 +2,7 @@ import axios, { AxiosError } from 'axios'
// 创建axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
baseURL: import.meta.env.VITE_API_BASE_URL || '/email/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
......
......@@ -6,7 +6,7 @@
v-model="currentSender"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option v-for="sender in senders" :key="sender.id" :value="sender">
<option v-for="sender in senders" :key="sender.senderBizId" :value="sender">
{{ sender.email }}
</option>
</select>
......@@ -20,8 +20,8 @@
@change="applyVariableTemplate"
>
<option value="">-- 选择模板 --</option>
<option v-for="template in variableTemplates" :key="template.id" :value="template">
{{ template.name }}
<option v-for="template in groups" :key="template.variableGroupBizId" :value="template">
{{ template.groupName }}
</option>
</select>
<!-- 当选择模版有值时,显示导入数据按钮 -->
......@@ -89,7 +89,7 @@
@click="showVariableSelector = true"
class="text-sm bg-blue-50 hover:bg-blue-100 text-blue-600 px-3 py-1 rounded border border-blue-200 transition-colors"
>
<i class="fas fa-variable mr-1"></i> 插入字段
<i class="fas fa-plus"></i> 插入字段
</button>
</div>
<textarea
......@@ -189,12 +189,12 @@
>
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<h3 class="text-lg font-semibold mb-4">导入联系人</h3>
<input
<!-- <input
type="file"
accept=".csv,.txt,.xlsx"
@change="handleImportContacts"
class="w-full px-3 py-2 border border-gray-300 rounded-md mb-4"
/>
/> -->
<div class="flex justify-end gap-3">
<button
@click="showImportContacts = false"
......@@ -230,17 +230,28 @@
@confirm-send="confirmSendEmail"
@close="showPreview = false"
/>
<!-- 弹窗组件 -->
<CommonModal
v-model:visible="modalVisible"
:trigger-key="modalConfig.triggerKey"
:title="modalConfig.title"
type="confirm"
:message="modalConfig.message"
:show-cancel-button="modalConfig.showCancel"
@confirm="handleConfirm"
@cancel="handleCancel"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ref, watch, onMounted } from 'vue'
import ContactSelector from './ContactSelector.vue'
import VariableSelector from './VariableSelector.vue'
import EmailPreview from './EmailPreview.vue'
import ImportRecordManager from './ImportRecordManager.vue'
import ImportDialog from './ImportDialog.vue'
import {
import type {
Sender,
Contact,
Variable,
......@@ -249,12 +260,75 @@ import {
EmailForm,
ImportRecord,
} from '../types'
// 引入api接口,获取联系人列表、发件人列表、变量模版列表
import {
getContactList,
getEmailSenderConfigList,
getEmailVariableGroupList,
getEmailVariableGroupDetail,
} from '../api/api'
// 引入弹窗组件
import CommonModal from '@/components/CommonModal.vue'
// 弹窗提示信息对象
const modalVisible = ref(false)
const modalConfig = ref({
showCancel: false,
title: '操作确认',
message: '确定要执行此操作吗?',
triggerKey: 'templateModal',
})
const openModal = (
config: { triggerKey?: string; showCancel?: boolean; title?: string; message?: string } = {},
) => {
modalConfig.value = {
showCancel: config.showCancel ?? false,
title: config.title ?? '操作确认',
message: config.message ?? '确定要执行此操作吗?',
triggerKey: config.triggerKey ?? modalConfig.value.triggerKey,
}
modalVisible.value = true
}
const handleConfirm = (triggerKey: string) => {
modalVisible.value = false
console.log('用户确认操作', triggerKey)
}
const handleCancel = (triggerKey: string) => {
modalVisible.value = false
console.log('用户取消操作', triggerKey)
}
// 初始化数据
const getSenders = () => {
getEmailSenderConfigList({
pageNo: 1,
pageSize: 100,
}).then((res) => {
senders.value = res.data?.records || []
})
}
const getContacts = () => {
getContactList({
pageNo: 1,
pageSize: 100,
}).then((res) => {
contacts.value = res.data?.records || []
})
}
const getGroups = () => {
getEmailVariableGroupList({
pageNo: 1,
pageSize: 100,
}).then((res) => {
groups.value = res.data?.records || []
})
}
const senders = ref<Sender[]>([])
const contacts = ref<Contact[]>([])
const groups = ref<VariableTemplate[]>([])
const variables = ref<Variable[]>([])
const variableTemplates = ref<VariableTemplate[]>([])
const emails = ref<Email[]>([])
// 状态
const currentSender = ref<Sender | null>(senders.value.length > 0 ? senders.value[0] : null)
......@@ -288,6 +362,13 @@ watch(
},
)
// 挂载时,加载获取基础数据接口
onMounted(() => {
getSenders()
getContacts()
getGroups()
})
const variablePrefix = '{{'
const variableNextfix = '}}'
......@@ -306,19 +387,38 @@ const removeAttachment = (index: number) => {
const applyVariableTemplate = () => {
if (!selectedVariableTemplate.value) return
const variableKeys =
selectedVariableTemplate.value.variableBizIdList.map((id) => {
const variable = variables.value.find((v) => v.variableBizId === id)
return variable ? variable.variableNameEn : ''
}) || []
const variablesText = variableKeys
.map((key) => `${variablePrefix}${key}${variableNextfix}`)
.join(', ')
emailForm.value.content = `【使用了模板变量:${variablesText}】\n\n${emailForm.value.content}`
if (selectedVariableTemplate.value.variableGroupBizId) {
getEmailVariableGroupDetail(selectedVariableTemplate.value.variableGroupBizId).then((res) => {
variables.value = res.data?.emailVariableDtoList || []
})
}
}
const insertVariable = (variable: Variable) => {
emailForm.value.content += `${variablePrefix}${variable.variableNameEn}${variableNextfix}`
// 支持多选变量,循环添加选中变量
const variablesToInsert = Array.isArray(variable) ? variable : [variable]
variablesToInsert.forEach((v) => {
const variableText = `${variablePrefix}${v.variableNameEn}${variableNextfix}`
// 在光标位置插入变量,如果没有光标则添加到末尾
const textarea = document.querySelector('textarea[name="content"]') as HTMLTextAreaElement
if (textarea && document.activeElement === textarea) {
const start = textarea.selectionStart
const end = textarea.selectionEnd
const currentValue = emailForm.value.content
emailForm.value.content =
currentValue.substring(0, start) + variableText + currentValue.substring(end)
// 设置光标位置到插入内容之后
const newCursorPos = start + variableText.length
textarea.setSelectionRange(newCursorPos, newCursorPos)
} else {
// 如果没有焦点在textarea,则添加到末尾
emailForm.value.content += variableText
}
})
showVariableSelector.value = false
}
......@@ -384,9 +484,9 @@ const saveAsDraft = () => {
}
const draft: Email = {
id: Date.now().toString(),
sender: currentSender.value.email,
to: emailForm.value.to,
cc: emailForm.value.cc,
sender: currentSender.value.email || '',
to: emailForm.value.to ? emailForm.value.to.split(',') : [],
cc: emailForm.value.cc ? emailForm.value.cc.split(',') : [],
subject: emailForm.value.subject || '无主题',
content: emailForm.value.content,
sendTime: new Date().toISOString(),
......@@ -414,9 +514,9 @@ const confirmSendEmail = () => {
}
const email: Email = {
id: Date.now().toString(),
sender: currentSender.value.email,
to: emailForm.value.to,
cc: emailForm.value.cc,
sender: currentSender.value.email || '',
to: emailForm.value.to ? emailForm.value.to.split(',') : [],
cc: emailForm.value.cc ? emailForm.value.cc.split(',') : [],
subject: emailForm.value.subject || '无主题',
content: emailForm.value.content,
sendTime:
......
......@@ -327,10 +327,9 @@ import {
getContactList,
getEmailSenderConfigList,
} from '@/api/api'
import { Contact, Sender } from '@/types/index'
import type { Contact } from '@/types/index'
// 引入弹窗组件
import CommonModal from '@/components/CommonModal.vue'
import { isTypeOnlyImportOrExportDeclaration } from 'typescript'
// 弹窗提示信息对象
const modalVisible = ref(false)
const modalConfig = ref({
......@@ -355,8 +354,8 @@ const openModal = (
const handleConfirm = (triggerKey: string) => {
modalVisible.value = false
console.log('用户确认操作', triggerKey)
if (triggerKey === 'deleteContactModal') {
deleteContact(form.value.contactBizId).then((res) => {
if (triggerKey === 'deleteContactModal' && form.value.contactBizId) {
deleteContact(form.value.contactBizId as string).then((res) => {
if (res.code === 200) {
fetchContacts()
openModal({
......@@ -368,7 +367,7 @@ const handleConfirm = (triggerKey: string) => {
openModal({
triggerKey: '',
title: '错误',
message: res.message || '联系人删除失败',
message: res.msg || '联系人删除失败',
})
}
})
......@@ -396,7 +395,7 @@ interface ImportResult {
}
errors?: ImportError[]
}
const editingSenderId = ref('')
// 页面状态
const contacts = ref<Contact[]>([])
const searchQuery = ref('')
......@@ -428,7 +427,21 @@ const errors = ref({
name: '',
email: '',
})
const resetForm = () => {
form.value = {
name: '',
type: '',
companyName: '',
email: '',
ccEmailList: [],
other: '',
appellation: '',
}
newCcEmail.value = ''
errors.value = { name: '', email: '' }
isEditing.value = false
showContactModal.value = true
}
// 导入相关状态
const importFile = ref<File | null>(null)
const isImporting = ref(false)
......
......@@ -19,21 +19,21 @@
<div class="space-y-2">
<div
v-for="contact in filteredContacts"
:key="contact.id"
:key="contact.contactBizId"
class="flex items-center p-3 border border-gray-200 rounded-md hover:bg-blue-50 cursor-pointer"
@click="toggleSelection(contact)"
>
<input
type="checkbox"
:id="'contact-' + contact.id"
:checked="selectedContacts.includes(contact.id)"
:id="'contact-' + contact.contactBizId"
:checked="selectedContacts.includes(contact.contactBizId || '')"
class="mr-3"
/>
<label for="'contact-' + contact.id" class="flex-1">
<label for="'contact-' + contact.contactBizId" class="flex-1">
<div class="font-medium">{{ contact.name }}</div>
<div class="text-sm text-gray-500">{{ contact.email }}</div>
</label>
<div class="text-sm text-gray-500">{{ contact.company || '' }}</div>
<div class="text-sm text-gray-500">{{ contact.companyName || '' }}</div>
</div>
</div>
<div v-if="filteredContacts.length === 0" class="p-6 text-center text-gray-500">
......@@ -60,7 +60,7 @@
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import { Contact } from '../types'
import type { Contact } from '@/types/index'
const props = defineProps({
contacts: {
......@@ -79,28 +79,30 @@ const selectedContacts = ref<string[]>([])
const filteredContacts = computed(() => {
return props.contacts.filter(
(contact) =>
contact.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
contact.email.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
contact.company.toLowerCase().includes(searchTerm.value.toLowerCase()),
contact.name?.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
contact.email?.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
contact.companyName?.toLowerCase().includes(searchTerm.value.toLowerCase()),
)
})
// 方法
const toggleSelection = (contact: Contact) => {
const index = selectedContacts.value.indexOf(contact.id)
const index = selectedContacts.value.indexOf(contact.contactBizId || '')
if (index > -1) {
selectedContacts.value.splice(index, 1)
} else {
selectedContacts.value.push(contact.id)
selectedContacts.value.push(contact.contactBizId || '')
}
}
const confirmSelection = () => {
const selected = props.contacts.filter((contact) => selectedContacts.value.includes(contact.id))
const selected = props.contacts.filter((contact) =>
selectedContacts.value.includes(contact.contactBizId || ''),
)
const to = selected.map((contact) => contact.email).join(',')
const cc = selected
.map((contact) => contact.ccEmail)
.filter(Boolean)
.map((contact) => contact.ccEmailList?.join(',') || '')
.filter((email) => email)
.join(',')
emits('confirm-selection', { to, cc })
......
......@@ -120,7 +120,7 @@
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import { Email } from '../types'
import type { Email } from '@/types/index'
// 状态
const emails = ref<Email[]>([])
......@@ -150,7 +150,9 @@ const formatDate = (dateString: string) => {
const viewEmailDetail = (email: Email) => {
// 显示邮件详情
alert(`邮件主题: ${email.subject}\n收件人: ${email.to}\n发送时间: ${formatDate(email.sendTime)}`)
alert(
`邮件主题: ${email.subject || '无'}\n收件人: ${email.to || '无'}\n发送时间: ${formatDate(email.sendTime || '')}`,
)
// 实际项目中可以打开详情弹窗
}
......
<template>
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-semibold">邮件预览</h3>
<button @click="$emit('close')">
<i class="fas fa-times text-gray-500"></i>
</button>
</div>
<div class="p-6 flex-1 overflow-y-auto">
<div class="mb-4">
<div class="text-sm text-gray-500">发件人:</div>
<div class="font-medium">{{ sender }}</div>
</div>
<div class="mb-4">
<div class="text-sm text-gray-500">收件人:</div>
<div>{{ emailForm.to }}</div>
</div>
<div v-if="emailForm.cc" class="mb-4">
<div class="text-sm text-gray-500">抄送人:</div>
<div>{{ emailForm.cc }}</div>
</div>
<div class="mb-6 pt-4 border-t border-gray-200">
<div class="text-xl font-semibold">{{ emailForm.subject }}</div>
</div>
<div class="mb-6">
<div v-html="previewContent" class="prose max-w-none"></div>
</div>
<div v-if="attachments.length > 0" class="pt-4 border-t border-gray-200">
<div class="text-sm text-gray-500 mb-2">附件:</div>
<div class="space-y-1">
<div
v-for="(file, index) in attachments"
:key="index"
class="flex items-center text-sm"
>
<i class="fas fa-file mr-2 text-gray-400"></i>
<span>{{ file.name }}</span>
</div>
</div>
</div>
<div
v-if="emailForm.scheduleSend"
class="mt-4 pt-4 border-t border-gray-200 text-sm text-gray-600"
>
<i class="fas fa-clock mr-1"></i> 定时发送: {{ emailForm.sendTime || '未设置时间' }}
</div>
</div>
<div class="p-4 border-t border-gray-200 flex justify-end gap-3">
<button
@click="$emit('close')"
class="px-6 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
>
返回编辑
</button>
<button
@click="$emit('confirm-send')"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors"
>
确认发送
</button>
</div>
</div>
</div>
<div class="email-preview"></div>
</template>
<script setup lang="ts">
import { computed, defineProps, defineEmits } from 'vue'
import { EmailForm } from '../types'
const props = defineProps({
emailForm: {
type: Object as () => EmailForm,
required: true,
},
sender: {
type: String,
required: true,
},
attachments: {
type: Array as () => File[],
required: true,
},
})
const emits = defineEmits(['confirm-send', 'close'])
// 计算属性
const previewContent = computed(() => {
// 替换变量为占位符用于预览
return props.emailForm.content.replace(
/{{\s*(\w+)\s*}}/g,
'<span class="bg-blue-100 px-1 rounded">[$1]</span>',
)
})
</script>
......@@ -114,7 +114,7 @@
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import { ImportRecord } from '../types'
import type { ImportRecord } from '@/types/index'
const props = defineProps({
records: {
......
......@@ -53,7 +53,7 @@
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
......
......@@ -98,7 +98,7 @@
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="sender in senders" :key="sender.id">
<tr v-for="sender in senders" :key="sender.senderBizId">
<td class="px-6 py-4 whitespace-nowrap">{{ sender.email }}</td>
<td class="px-6 py-4 whitespace-nowrap">
{{
......@@ -119,7 +119,7 @@
编辑
</button>
<button
@click="deleteSender(sender.senderBizId)"
@click="deleteSender(sender.senderBizId || '')"
class="text-red-600 hover:text-red-900"
>
删除
......@@ -156,7 +156,7 @@ import {
addEmailSenderConfig,
editEmailSenderConfig,
} from '@/api/api'
import { Provider, Sender } from '@/types'
import type { Sender } from '@/types/index'
// 引入弹窗组件
import CommonModal from '@/components/CommonModal.vue'
// 弹窗提示信息对象
......
......@@ -96,7 +96,7 @@
<i class="fas fa-edit"></i>
</button> -->
<button
@click="deleteVariableTemplate(template.variableGroupBizId)"
@click="deleteVariableTemplate(template.variableGroupBizId || '')"
class="text-red-600 hover:text-red-900 text-sm"
>
<i class="fas fa-trash"></i>
......@@ -106,7 +106,7 @@
<p class="text-sm text-gray-600 mb-3">{{ template.description || '无描述' }}</p>
<div class="flex flex-wrap gap-2">
<span
v-for="variableId in template.variableNameEns.split(';')"
v-for="variableId in template.variableBizIdList"
:key="variableId"
class="px-2 py-1 bg-blue-50 text-blue-700 rounded text-xs"
>
......@@ -545,35 +545,6 @@ const showCreateTemplateModal = (isNew: boolean) => {
const editVariableTemplate = (template: VariableTemplate) => {
editingTemplateId.value = template.variableGroupBizId || ''
showTemplateModal.value = true
// 详情查询
if (editingTemplateId.value) {
// 详情查询出来的 variableBizIdList 数组,需要和 variables 数组匹配,找到匹配的变量,然后把状态改为勾选状态,选中变量置顶
getEmailVariableGroupDetail(editingTemplateId.value).then((res) => {
templateForm.value = {
...res.data,
variableBizIdList: res.data.emailVariableDtoList || [],
}
// 实现变量匹配逻辑:
// 1. 为每个变量对象添加selected属性
const selectedVariableIds =
res.data.emailVariableDtoList?.map((item) => item.variableBizId) || []
console.log(selectedVariableIds)
// 2. 更新variables数组,设置选中状态
variables.value = variables.value.map((variable) => ({
...variable,
selected: selectedVariableIds.includes(variable.variableBizId),
}))
// 3. 将选中的变量置顶显示(按选中状态排序)
variables.value.sort((a, b) => {
if (a.selected && !b.selected) return -1
if (!a.selected && b.selected) return 1
return 0
})
})
}
}
const closeTemplateModal = () => {
......
......@@ -17,35 +17,57 @@
/>
</div>
<div class="grid grid-cols-1 gap-2">
<button
<div
v-for="variable in filteredVariables"
:key="variable.id"
class="p-3 border border-gray-200 rounded-md hover:bg-blue-50 text-left transition-colors"
@click="selectVariable(variable)"
:key="variable.variableBizId"
class="flex items-center p-3 border border-gray-200 rounded-md hover:bg-blue-50 cursor-pointer transition-colors"
>
<div class="font-medium font-mono">{{ variablePrefix }}{{ variable.key }}</div>
<div class="text-sm text-gray-500">{{ variable.name }}</div>
</button>
<input
type="checkbox"
:id="variable.variableBizId"
:checked="selectedVariables.includes(variable.variableBizId || '')"
class="mr-3 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
@change="toggleVariable(variable)"
@click.stop
/>
<label :for="variable.variableBizId" class="flex-1 cursor-pointer">
<div class="text-sm text-gray-500">{{ variable.variableNameCn }}</div>
<div class="font-medium font-mono">
{{ variablePrefix }}{{ variable.variableNameEn }}{{ variableNextfix }}
</div>
</label>
</div>
</div>
<div v-if="filteredVariables.length === 0" class="p-6 text-center text-gray-500">
<p>未找到匹配的变量</p>
</div>
</div>
<div class="p-4 border-t border-gray-200 flex justify-end">
<div class="p-4 border-t border-gray-200 flex justify-between items-center">
<div class="text-sm text-gray-500">已选择 {{ selectedVariables.length }} 个变量</div>
<div class="flex gap-3">
<button
@click="$emit('close')"
@click="clearSelection"
class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
:disabled="selectedVariables.length === 0"
>
清空
</button>
<button
@click="confirmSelection"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors"
:disabled="selectedVariables.length === 0"
>
关闭
插入变量
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import { Variable } from '../types'
import type { Variable } from '@/types/index'
const props = defineProps({
variables: {
......@@ -58,19 +80,38 @@ const emits = defineEmits(['insert-variable', 'close'])
// 状态
const searchTerm = ref('')
const selectedVariables = ref<string[]>([])
const variablePrefix = '{{'
const variableNextfix = '}}'
// 计算属性
const filteredVariables = computed(() => {
return props.variables.filter(
(variable) =>
variable.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
variable.key.toLowerCase().includes(searchTerm.value.toLowerCase()),
variable.variableNameCn?.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
variable.variableNameEn?.toLowerCase().includes(searchTerm.value.toLowerCase()),
)
})
// 方法
const selectVariable = (variable: Variable) => {
emits('insert-variable', variable)
const toggleVariable = (variable: Variable) => {
const index = selectedVariables.value.indexOf(variable.variableBizId || '')
if (index > -1) {
selectedVariables.value.splice(index, 1)
} else {
selectedVariables.value.push(variable.variableBizId || '')
}
}
const confirmSelection = () => {
const selected = props.variables.filter((variable) =>
selectedVariables.value.includes(variable.variableBizId || ''),
)
emits('insert-variable', selected)
emits('close')
}
const clearSelection = () => {
selectedVariables.value = []
}
</script>
......@@ -10,5 +10,9 @@
{
"path": "./tsconfig.vitest.json"
}
]
],
"compilerOptions": {
"skipLibCheck": true,
"strict": false
}
}
......@@ -7,7 +7,7 @@ import vueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig({
// 关键配置:设置基础路径为子目录 yd-email
base: process.env.NODE_ENV === 'production' ? '/yd-email/' : '/',
base: process.env.NODE_ENV === 'production' ? '/yd-email/' : '/yd-email/',
plugins: [vue(), vueJsx(), vueDevTools()],
resolve: {
alias: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment