|
|
@@ -0,0 +1,572 @@
|
|
|
+<template>
|
|
|
+ <div class="space-y-6">
|
|
|
+ <!-- 主卡片 -->
|
|
|
+ <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
|
|
+ <div class="border-b border-gray-200 pb-4 mb-6">
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
+ <h2 class="text-xl font-semibold text-gray-800 flex items-center gap-2">
|
|
|
+ <el-icon><Grid /></el-icon>
|
|
|
+ 账号组管理
|
|
|
+ </h2>
|
|
|
+ <el-button type="primary" @click="handleCreate">
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
+ 创建账号组
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 账号组列表 -->
|
|
|
+ <el-table
|
|
|
+ :data="groupList"
|
|
|
+ style="width: 100%"
|
|
|
+ v-loading="loading"
|
|
|
+ stripe
|
|
|
+ border
|
|
|
+ >
|
|
|
+ <el-table-column prop="id" label="ID" width="80" />
|
|
|
+ <el-table-column prop="groupName" label="账号组名称" width="200" />
|
|
|
+ <el-table-column label="投放模式" width="200">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="getDeliveryModeType(row.deliveryMode)">
|
|
|
+ {{ getDeliveryModeText(row.deliveryMode) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="remark" label="备注" show-overflow-tooltip />
|
|
|
+ <el-table-column label="创建时间" width="180">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ formatDate(row.createTime) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="300" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" size="small" link @click="handleEdit(row)">
|
|
|
+ <el-icon><Edit /></el-icon>
|
|
|
+ 编辑
|
|
|
+ </el-button>
|
|
|
+ <el-button type="danger" size="small" link @click="handleDelete(row)">
|
|
|
+ <el-icon><Delete /></el-icon>
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <div class="mt-6 flex justify-end">
|
|
|
+ <el-pagination
|
|
|
+ v-if="total > 0"
|
|
|
+ v-model:current-page="currentPage"
|
|
|
+ v-model:page-size="pageSize"
|
|
|
+ :page-sizes="[10, 20, 50]"
|
|
|
+ :total="total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 创建/编辑账号组对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="dialogVisible"
|
|
|
+ :title="dialogTitle"
|
|
|
+ width="80%"
|
|
|
+ @close="handleDialogClose"
|
|
|
+ >
|
|
|
+ <el-form
|
|
|
+ ref="formRef"
|
|
|
+ :model="formData"
|
|
|
+ :rules="formRules"
|
|
|
+ label-width="120px"
|
|
|
+ >
|
|
|
+ <el-form-item label="账号组名称" prop="groupName">
|
|
|
+ <el-input
|
|
|
+ v-model="formData.groupName"
|
|
|
+ placeholder="请输入账号组名称"
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="投放模式" prop="deliveryMode">
|
|
|
+ <el-select
|
|
|
+ v-model="formData.deliveryMode"
|
|
|
+ placeholder="请选择投放模式"
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-option label="线索-企微-加企微" :value="1" />
|
|
|
+ <el-option label="用户增长-短剧-注册变现ROI" :value="2" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="备注信息" prop="remark">
|
|
|
+ <el-input
|
|
|
+ v-model="formData.remark"
|
|
|
+ type="textarea"
|
|
|
+ :rows="3"
|
|
|
+ placeholder="请输入备注信息"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-input
|
|
|
+ v-model="searchKeyword"
|
|
|
+ placeholder="搜索账户ID"
|
|
|
+ clearable
|
|
|
+ @input="handleSearchAccounts"
|
|
|
+ style="width: 300px"
|
|
|
+ >
|
|
|
+ <template #prefix>
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ <span class="ml-2 text-sm text-gray-500">已加载 {{ allAccounts.length }} 条账号,已选择 {{ formData.accountIds.length }} 条</span>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="选择账号">
|
|
|
+
|
|
|
+
|
|
|
+ <!-- 左中右三列布局 -->
|
|
|
+ <div class="grid grid-cols-4 gap-4">
|
|
|
+ <!-- 批量粘贴区域 -->
|
|
|
+ <div class="mb-3">
|
|
|
+ <div class="text-sm text-gray-600 mb-1">批量粘贴账号ID(换行分隔):</div>
|
|
|
+ <el-input
|
|
|
+ v-model="batchAccountIds"
|
|
|
+ type="textarea"
|
|
|
+ :rows="3"
|
|
|
+ placeholder="粘贴账号ID,每行一个"
|
|
|
+ class="mb-2"
|
|
|
+ />
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ type="primary"
|
|
|
+ @click="handleBatchAdd"
|
|
|
+ :disabled="!batchAccountIds.trim()"
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ 批量添加
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <!-- 左侧:可选账号 -->
|
|
|
+ <div class="border rounded-lg p-4">
|
|
|
+ <div class="flex items-center justify-between mb-3 pb-2 border-b">
|
|
|
+ <span class="font-medium text-gray-700">可选账号</span>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ @click="selectAll"
|
|
|
+ :disabled="allAccounts.length === 0"
|
|
|
+ >
|
|
|
+ 全选
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+
|
|
|
+ <div class="max-h-96 overflow-y-auto">
|
|
|
+ <el-checkbox-group v-model="selectAccounts">
|
|
|
+ <div
|
|
|
+ v-for="account in allAccounts"
|
|
|
+ :key="account.accountId"
|
|
|
+ class="mb-2"
|
|
|
+ >
|
|
|
+ <el-checkbox
|
|
|
+ :label="account.accountId"
|
|
|
+ class="w-full"
|
|
|
+ >
|
|
|
+ <span class="text-sm">
|
|
|
+ {{ account.accountId }}
|
|
|
+ </span>
|
|
|
+ </el-checkbox>
|
|
|
+ </div>
|
|
|
+ </el-checkbox-group>
|
|
|
+ <div v-if="allAccounts.length === 0" class="text-center text-gray-400 py-8">
|
|
|
+ 暂无可选账号
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 选择确认按钮 垂直居中 -->
|
|
|
+ <el-button type="primary" @click="handleSelectAccounts" class="self-center justify-self-center">添加</el-button>
|
|
|
+
|
|
|
+
|
|
|
+ <!-- 右侧:已选账号 -->
|
|
|
+ <div class="border rounded-lg p-4 bg-blue-50">
|
|
|
+ <div class="flex items-center justify-between mb-3 pb-2 border-b border-blue-200">
|
|
|
+ <span class="font-medium text-gray-700">已选账号</span>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ type="danger"
|
|
|
+ @click="clearAll"
|
|
|
+ :disabled="formData.accountIds.length === 0"
|
|
|
+ >
|
|
|
+ 清空
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div class="max-h-96 overflow-y-auto">
|
|
|
+ <div
|
|
|
+ v-for="accountId in formData.accountIds"
|
|
|
+ :key="accountId"
|
|
|
+ class="mb-2 flex items-center justify-between bg-white rounded px-3 py-2"
|
|
|
+ >
|
|
|
+ <span class="text-sm">
|
|
|
+ {{ accountId }}
|
|
|
+ </span>
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ size="small"
|
|
|
+ link
|
|
|
+ @click="removeAccount(accountId)"
|
|
|
+ >
|
|
|
+ <el-icon><Close /></el-icon>
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div v-if="formData.accountIds.length === 0" class="text-center text-gray-400 py-8">
|
|
|
+ 请从左侧选择账号
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleSubmit" :loading="submitting">
|
|
|
+ 确定
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, onMounted, watch } from 'vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import {
|
|
|
+ getAccountGroupList,
|
|
|
+ createAccountGroup,
|
|
|
+ updateAccountGroup,
|
|
|
+ deleteAccountGroup,
|
|
|
+ getGroupAccounts
|
|
|
+} from '@/api/accountGroup'
|
|
|
+import { getAllAccounts } from '@/api/account'
|
|
|
+import {
|
|
|
+ Grid,
|
|
|
+ Plus,
|
|
|
+ View,
|
|
|
+ Edit,
|
|
|
+ Delete,
|
|
|
+ Search,
|
|
|
+ Close
|
|
|
+} from '@element-plus/icons-vue'
|
|
|
+
|
|
|
+// 数据
|
|
|
+const loading = ref(false)
|
|
|
+const submitting = ref(false)
|
|
|
+const groupList = ref([])
|
|
|
+const currentPage = ref(1)
|
|
|
+const pageSize = ref(10)
|
|
|
+const total = ref(0)
|
|
|
+
|
|
|
+// 对话框
|
|
|
+const dialogVisible = ref(false)
|
|
|
+const dialogTitle = ref('创建账号组')
|
|
|
+const formRef = ref(null)
|
|
|
+const editingId = ref(null)
|
|
|
+
|
|
|
+// 表单数据
|
|
|
+const formData = ref({
|
|
|
+ groupName: '',
|
|
|
+ deliveryMode: null,
|
|
|
+ remark: '',
|
|
|
+ accountIds: []
|
|
|
+})
|
|
|
+
|
|
|
+// 表单验证规则
|
|
|
+const formRules = {
|
|
|
+ groupName: [
|
|
|
+ { required: true, message: '请输入账号组名称', trigger: 'blur' }
|
|
|
+ ],
|
|
|
+ deliveryMode: [
|
|
|
+ { required: true, message: '请选择投放模式', trigger: 'change' }
|
|
|
+ ]
|
|
|
+}
|
|
|
+
|
|
|
+// 所有账号列表
|
|
|
+const allAccounts = ref([])
|
|
|
+const selectAccounts = ref([])
|
|
|
+const searchKeyword = ref('')
|
|
|
+const searchTimer = ref(null)
|
|
|
+const batchAccountIds = ref('') // 批量账号ID
|
|
|
+
|
|
|
+// 查看账号对话框
|
|
|
+const accountsDialogVisible = ref(false)
|
|
|
+const accountsLoading = ref(false)
|
|
|
+const groupAccounts = ref([])
|
|
|
+const currentGroupId = ref(null)
|
|
|
+
|
|
|
+
|
|
|
+// 方法
|
|
|
+const loadGroupList = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getAccountGroupList(currentPage.value, pageSize.value)
|
|
|
+ if (res.success) {
|
|
|
+ groupList.value = res.data?.records || []
|
|
|
+ total.value = res.data?.total || 0
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '加载账号组列表失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载账号组列表失败:', error)
|
|
|
+ ElMessage.error('加载账号组列表失败')
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const loadAllAccounts = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getAllAccounts()
|
|
|
+ if (res.success) {
|
|
|
+ const accounts = res.data || []
|
|
|
+ allAccounts.value = accounts.map(account => ({
|
|
|
+ accountId: account.accountId,
|
|
|
+ label: `${account.accountId} - ${account.corporationName || '未命名'}`,
|
|
|
+ ...account
|
|
|
+ }))
|
|
|
+ selectAccounts.value = []
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载账号列表失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 搜索账号(防抖处理)
|
|
|
+const handleSearchAccounts = () => {
|
|
|
+ if (searchTimer.value) {
|
|
|
+ clearTimeout(searchTimer.value)
|
|
|
+ }
|
|
|
+
|
|
|
+ searchTimer.value = setTimeout(async () => {
|
|
|
+ try {
|
|
|
+ const keyword = searchKeyword.value?.trim()
|
|
|
+ const res = keyword
|
|
|
+ ? await getAllAccounts(keyword)
|
|
|
+ : await getAllAccounts()
|
|
|
+
|
|
|
+ if (res.success) {
|
|
|
+ const accounts = res.data || []
|
|
|
+ allAccounts.value = accounts.map(account => ({
|
|
|
+ accountId: account.accountId,
|
|
|
+ label: `${account.accountId} - ${account.corporationName || '未命名'}`,
|
|
|
+ ...account
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('搜索账号失败:', error)
|
|
|
+ }
|
|
|
+ }, 500) // 500ms 防抖
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+// 全选
|
|
|
+const selectAll = () => {
|
|
|
+ const allAvailableIds = allAccounts.value.map(acc => acc.accountId)
|
|
|
+ selectAccounts.value = [...allAvailableIds]
|
|
|
+}
|
|
|
+
|
|
|
+// 清空
|
|
|
+const clearAll = () => {
|
|
|
+ formData.value.accountIds = []
|
|
|
+ selectAccounts.value = []
|
|
|
+}
|
|
|
+
|
|
|
+const handleSelectAccounts = () => {
|
|
|
+ formData.value.accountIds = [...new Set([...formData.value.accountIds, ...selectAccounts.value])]
|
|
|
+ selectAccounts.value = []
|
|
|
+}
|
|
|
+
|
|
|
+// 批量添加账号
|
|
|
+const handleBatchAdd = () => {
|
|
|
+ const ids = batchAccountIds.value
|
|
|
+ .split('\n')
|
|
|
+ .map(id => id.trim())
|
|
|
+ .filter(id => id !== '')
|
|
|
+ .map(id => parseInt(id))
|
|
|
+ .filter(id => !isNaN(id))
|
|
|
+
|
|
|
+ if (ids.length === 0) {
|
|
|
+ ElMessage.warning('请输入有效的账号ID')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 合并到已选列表,去重
|
|
|
+ formData.value.accountIds = [...new Set([...formData.value.accountIds, ...ids])]
|
|
|
+ selectAccounts.value = [...formData.value.accountIds]
|
|
|
+
|
|
|
+ // 清空textarea
|
|
|
+ batchAccountIds.value = ''
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// 移除单个账号
|
|
|
+const removeAccount = (accountId) => {
|
|
|
+ const index = formData.value.accountIds.indexOf(accountId)
|
|
|
+ if (index > -1) {
|
|
|
+ formData.value.accountIds.splice(index, 1)
|
|
|
+ selectAccounts.value = selectAccounts.value.filter(accId => accId !== accountId)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getDeliveryModeText = (mode) => {
|
|
|
+ const modeMap = {
|
|
|
+ 1: '线索-企微-加企微',
|
|
|
+ 2: '用户增长-短剧-注册变现ROI'
|
|
|
+ }
|
|
|
+ return modeMap[mode] || '未知'
|
|
|
+}
|
|
|
+
|
|
|
+const getDeliveryModeType = (mode) => {
|
|
|
+ const typeMap = {
|
|
|
+ 1: 'success',
|
|
|
+ 2: 'warning'
|
|
|
+ }
|
|
|
+ return typeMap[mode] || 'info'
|
|
|
+}
|
|
|
+
|
|
|
+const formatDate = (dateStr) => {
|
|
|
+ if (!dateStr) return '-'
|
|
|
+ return new Date(dateStr).toLocaleString('zh-CN')
|
|
|
+}
|
|
|
+
|
|
|
+const handleCreate = async () => {
|
|
|
+ await loadAllAccounts()
|
|
|
+ dialogTitle.value = '创建账号组'
|
|
|
+ editingId.value = null
|
|
|
+ formData.value = {
|
|
|
+ groupName: '',
|
|
|
+ deliveryMode: null,
|
|
|
+ remark: '',
|
|
|
+ accountIds: []
|
|
|
+ }
|
|
|
+ dialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const handleEdit = async (row) => {
|
|
|
+ await loadAllAccounts()
|
|
|
+ dialogTitle.value = '编辑账号组'
|
|
|
+ editingId.value = row.id
|
|
|
+
|
|
|
+ // 加载该账号组的账号列表
|
|
|
+ try {
|
|
|
+ const res = await getGroupAccounts(row.id)
|
|
|
+ const accountIds = res.success ? (res.data || []) : []
|
|
|
+
|
|
|
+ formData.value = {
|
|
|
+ groupName: row.groupName,
|
|
|
+ deliveryMode: row.deliveryMode,
|
|
|
+ remark: row.remark,
|
|
|
+ accountIds: accountIds
|
|
|
+ }
|
|
|
+ dialogVisible.value = true
|
|
|
+ selectAccounts.value = []
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载账号组账号列表失败:', error)
|
|
|
+ ElMessage.error('加载账号组信息失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleDelete = async (row) => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `确定要删除账号组"${row.groupName}"吗?`,
|
|
|
+ '提示',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ const res = await deleteAccountGroup(row.id)
|
|
|
+ if (res.success) {
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ await loadGroupList()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '删除失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('删除账号组失败:', error)
|
|
|
+ ElMessage.error('删除失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleSubmit = async () => {
|
|
|
+ try {
|
|
|
+ await formRef.value.validate()
|
|
|
+
|
|
|
+ submitting.value = true
|
|
|
+ const data = {
|
|
|
+ groupName: formData.value.groupName,
|
|
|
+ deliveryMode: formData.value.deliveryMode,
|
|
|
+ remark: formData.value.remark,
|
|
|
+ accountIds: formData.value.accountIds
|
|
|
+ }
|
|
|
+
|
|
|
+ let res
|
|
|
+ if (editingId.value) {
|
|
|
+ // 编辑
|
|
|
+ data.groupId = editingId.value
|
|
|
+ res = await updateAccountGroup(data)
|
|
|
+ } else {
|
|
|
+ // 创建
|
|
|
+ res = await createAccountGroup(data)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (res.success) {
|
|
|
+ ElMessage.success(editingId.value ? '更新成功' : '创建成功')
|
|
|
+ dialogVisible.value = false
|
|
|
+ await loadGroupList()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '操作失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== false) {
|
|
|
+ console.error('提交失败:', error)
|
|
|
+ ElMessage.error('操作失败')
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ submitting.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleDialogClose = () => {
|
|
|
+ formRef.value?.resetFields()
|
|
|
+ formData.value = {
|
|
|
+ groupName: '',
|
|
|
+ deliveryMode: null,
|
|
|
+ remark: '',
|
|
|
+ accountIds: []
|
|
|
+ },
|
|
|
+ selectAccounts.value = []
|
|
|
+}
|
|
|
+
|
|
|
+// 监听分页变化
|
|
|
+watch([currentPage, pageSize], () => {
|
|
|
+ loadGroupList()
|
|
|
+})
|
|
|
+
|
|
|
+// 生命周期
|
|
|
+onMounted(() => {
|
|
|
+ loadGroupList()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* 自定义checkbox样式 */
|
|
|
+:deep(.el-checkbox) {
|
|
|
+ width: 100%;
|
|
|
+ margin-right: 0;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-checkbox__label) {
|
|
|
+ width: 100%;
|
|
|
+ white-space: normal;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+</style>
|