Procházet zdrojové kódy

dp账号增加指定供应商

dodo hold před 1 rokem
rodič
revize
a76291f2be

+ 44 - 0
src/api/apiAccount.ts

@@ -0,0 +1,44 @@
+import axios from 'axios';
+import type { ListRes, FormRes } from './base';
+
+export interface ApiAccountRecord {
+    id: number;
+    name: string;
+    description: string;
+    api_key: string;
+    api_secret: string;
+    report_account_ids: string;
+    report_exclude_account_ids: string;
+    status: boolean;
+    enable_report: boolean;
+    create_time: Date;
+    last_time: Date;
+}
+
+
+export interface ApiAccountParams extends Partial<ApiAccountRecord> {
+    current: number;
+    pageSize: number;
+    sort?: string;
+    order?: string;
+    getTotal: boolean;
+}
+
+export interface ApiAccountRes extends ListRes {
+    list: ApiAccountRecord[];
+}
+
+export function queryAccountList(data: ApiAccountParams) {
+    return axios.post<ApiAccountRes>('/api/apiAccount/list', data);
+}
+
+export function UpdateAccount(data: ApiAccountRecord) {
+    return axios.post<FormRes>('/api/apiAccount/update', data);
+}
+
+
+
+export function CreateAccount(data: ApiAccountRecord) {
+    return axios.post<FormRes>('/api/apiAccount/create', data);
+}
+

+ 1 - 0
src/api/taobao.ts

@@ -36,6 +36,7 @@ export interface UpdateConfigModel {
 
     tk_limit_per_ip_24h: number;
     tk_limit_per_oaid_24h: number;
+    tk_parse_retry_count: number;
 
     cpsIgnorePercentageCity: string;
     cps_limit_per_ip_24h: number;

+ 7 - 0
src/locale/zh-CN/taoke.ts

@@ -2,6 +2,13 @@ export default {
   'menu.settings': '系统设置',
   'menu.settings.config': '全局配置',
   'menu.settings.aliyun': '阿里云资源管理',
+  'menu.settings.apiAccount': '接口授权管理',
+
+  'menu.apiAccount': '接口授权管理',
+  'menu.apiAccount.update': '更新账号',
+  'menu.apiAccount.list': '授权账号',
+
+  
 
   'menu.jd': '京东联盟',
   'menu.jd.updateCookies': '更新 JD Cookies',

+ 14 - 4
src/router/routes/modules/settings.ts

@@ -12,6 +12,16 @@ const SETTINGS: AppRouteRecordRaw = {
     order: 99,
   },
   children: [
+    {
+      path: 'config',
+      name: 'config',
+      component: () => import('@/views/settings/config.vue'),
+      meta: {
+        locale: 'menu.settings.config',
+        requiresAuth: true,
+        roles: ['*'],
+      },
+    },
     {
       path: 'aliyun',
       name: 'aliyun',
@@ -23,11 +33,11 @@ const SETTINGS: AppRouteRecordRaw = {
       },
     },
     {
-      path: 'config',
-      name: 'config',
-      component: () => import('@/views/settings/config.vue'),
+      path: 'apiAccount',
+      name: 'apiAccount',
+      component: () => import('@/views/settings/apiAccount.vue'),
       meta: {
-        locale: 'menu.settings.config',
+        locale: 'menu.settings.apiAccount',
         requiresAuth: true,
         roles: ['*'],
       },

+ 578 - 0
src/views/settings/apiAccount.vue

@@ -0,0 +1,578 @@
+<template>
+    <div class="container">
+        <Breadcrumb :items="['menu.apiAccount', 'menu.apiAccount.list']" />
+        <a-card class="general-card" :title="$t('menu.apiAccount.list')">
+            <a-row>
+                <a-col :flex="1">
+                    <a-form :model="formModel" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
+                        label-align="left">
+                        <a-row :gutter="16">
+                            <a-col :span="8">
+                                <a-form-item field="keyword" label="搜索">
+                                    <a-input v-model="formModel.keyword" placeholder="输入关键词搜索" />
+                                </a-form-item>
+                            </a-col>
+                            <a-col :span="8">
+                                <a-form-item field="status" :label="$t('searchTable.form.status')">
+                                    <a-select v-model="formModel.status" :options="statusOptions" :placeholder="$t('searchTable.form.selectDefault')
+                                        " />
+                                </a-form-item>
+                            </a-col>
+                        </a-row>
+                    </a-form>
+                </a-col>
+                <a-divider style="height: 38px" direction="vertical" />
+                <a-col :flex="'86px'" style="text-align: right">
+                    <a-space direction="vertical" :size="18">
+                        <a-button type="primary" @click="search">
+                            <template #icon>
+                                <icon-search />
+                            </template>
+                            {{ $t('searchTable.form.search') }}
+                        </a-button>
+                    </a-space>
+                </a-col>
+            </a-row>
+            <a-divider style="margin-top: 0" />
+            <a-row style="margin-bottom: 16px">
+                <a-col :span="12">
+                    <a-space>
+                        <a-button type="primary" @click="onAdd">
+                            <template #icon>
+                                <icon-plus />
+                            </template>
+                            {{ $t('searchTable.operation.create') }}
+                        </a-button>
+                    </a-space>
+                </a-col>
+                <a-col :span="12" style="
+                        display: flex;
+                        align-items: center;
+                        justify-content: end;
+                    ">
+                    <a-tooltip :content="$t('searchTable.actions.refresh')">
+                        <div class="action-icon" @click="search"><icon-refresh size="18" /></div>
+                    </a-tooltip>
+                    <a-dropdown @select="handleSelectDensity">
+                        <a-tooltip :content="$t('searchTable.actions.density')">
+                            <div class="action-icon"><icon-line-height size="18" /></div>
+                        </a-tooltip>
+                        <template #content>
+                            <a-doption v-for="item in densityList" :key="item.value" :value="item.value"
+                                :class="{ active: item.value === size }">
+                                <span>{{ item.name }}</span>
+                            </a-doption>
+                        </template>
+                    </a-dropdown>
+                    <a-tooltip :content="$t('searchTable.actions.columnSetting')">
+                        <a-popover trigger="click" position="bl" @popup-visible-change="popupVisibleChange">
+                            <div class="action-icon"><icon-settings size="18" /></div>
+                            <template #content>
+                                <div id="tableSetting">
+                                    <div v-for="(item, index) in showColumns" :key="item.dataIndex" class="setting">
+                                        <div style="
+                                                margin-right: 4px;
+                                                cursor: move;
+                                            ">
+                                            <icon-drag-arrow />
+                                        </div>
+                                        <div>
+                                            <a-checkbox v-model="item.checked" @change="
+                                                handleChange(
+                                                    $event,
+                                                    item as TableColumnData,
+                                                    index
+                                                )
+                                                ">
+                                            </a-checkbox>
+                                        </div>
+                                        <div class="title">
+                                            {{
+                                                item.title === '#'
+                                                    ? '序列号'
+                                                    : item.title
+                                            }}
+                                        </div>
+                                    </div>
+                                </div>
+                            </template>
+                        </a-popover>
+                    </a-tooltip>
+                </a-col>
+            </a-row>
+
+            <a-table row-key="id" :loading="loading" :pagination="pagination"
+                :columns="(cloneColumns as TableColumnData[])" :data="renderData" :bordered="{ headerCell: true }"
+                :size="size" @page-change="onPageChange">
+                <template #index="{ record }">
+                    {{ record.id }}
+                </template>
+                <template #name="{ record }">
+                    <div>{{ record.name }}</div>
+                    <a-typography-paragraph :ellipsis="{ rows: 2 }">
+                        <a-typography-text disabled>
+                            {{ record.description }}
+                        </a-typography-text>
+                    </a-typography-paragraph>
+                </template>
+
+                <template #api_key="{ record }">
+                    <div>{{ record.api_key }}</div>
+                    <a-typography-text disabled>
+                        {{ record.api_secret }}
+                    </a-typography-text>
+                </template>
+
+                <template #enable_report="{ record }">
+                    <template v-if="record.enable_report">
+                        <a-tag bordered color="blue">
+                            补点击报表
+                        </a-tag>
+                        <div>指定账号:{{ record.report_account_ids }}</div>
+                        <a-typography-paragraph>
+                            <a-typography-text type="warning">
+                                排除账号: {{ record.report_exclude_account_ids }}
+                            </a-typography-text>
+                        </a-typography-paragraph>
+                    </template>
+
+                    <a-tag v-else bordered>
+                        通用API
+                    </a-tag>
+                </template>
+                <template #status="{ record }">
+                    <a-tag v-if="record.status" bordered color="green">
+                        <template #icon>
+                            <icon-check />
+                        </template>
+                        正常
+                    </a-tag>
+                    <a-tag v-else bordered color="">
+                        <template #icon>
+                            <icon-stop />
+                        </template>
+                        禁用
+                    </a-tag>
+                </template>
+                <template #time="{ record }">
+                    <div style="color:gray;">
+                        <div>添加:{{ dayjs(record.create_time).format('MM-DD HH:mm') }}</div>
+                        <div>更新:{{ dayjs(record.last_time).format('MM-DD HH:mm') }}</div>
+                    </div>
+                </template>
+                <template #command="{ record }"> <a-space>
+                        <a-button type="primary" @click="onEdit(record)">
+                            修改
+                        </a-button>
+                        <a-button status="danger" @click="4(record)">
+                            删除
+                        </a-button>
+                    </a-space>
+                </template>
+            </a-table>
+        </a-card>
+
+        <a-modal v-model:visible="visible" :title="isEdit ? '修改API账户' : '新建API账户'" @ok="handleSubmit"
+            @cancel="handleCancel" width="800px">
+            <a-form :model="editForm" layout="vertical">
+                <a-row :gutter="24">
+                    <a-col :span="12">
+                        <a-form-item label="账户名称" required>
+                            <a-input v-model="editForm.name" placeholder="请输入账户名称" />
+                        </a-form-item>
+                    </a-col>
+                    <a-col :span="12">
+                        <a-form-item label="状态">
+                            <a-switch v-model="editForm.status" :checked-value="true" :unchecked-value="false"
+                                checked-text="启用" unchecked-text="禁用" />
+                        </a-form-item>
+                    </a-col>
+                    <a-col :span="24">
+                        <a-form-item label="描述信息">
+                            <a-textarea v-model="editForm.description" placeholder="请输入描述信息" />
+                        </a-form-item>
+                    </a-col>
+                    <a-col :span="12">
+                        <a-form-item label="API Key" required>
+                            <a-input v-model="editForm.api_key" placeholder="请输入API Key" />
+                        </a-form-item>
+                    </a-col>
+                    <a-col :span="12">
+                        <a-form-item label="API Secret" required>
+                            <a-input v-model="editForm.api_secret" placeholder="请输入API Secret" />
+                        </a-form-item>
+                    </a-col>
+                    <a-col :span="12">
+                        <a-form-item label="启用补点击报表">
+                            <a-switch v-model="editForm.enable_report" :checked-value="true" :unchecked-value="false"
+                                checked-text="启用" unchecked-text="禁用" />
+                        </a-form-item>
+                    </a-col>
+                    <template v-if="editForm.enable_report">
+                        <a-col :span="24">
+                            <a-form-item label="补点击报表包含账户ID">
+                                <selectAccount v-model="editForm.report_account_ids" />
+                            </a-form-item>
+                        </a-col>
+                        <a-col :span="24">
+                            <a-form-item label="补点击排除账户ID">
+                                <selectAccount v-model="editForm.report_exclude_account_ids" />
+                            </a-form-item>
+                        </a-col>
+                    </template>
+                </a-row>
+            </a-form>
+        </a-modal>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { useRouter } from 'vue-router';
+import { computed, ref, reactive, watch, nextTick } from 'vue';
+import { useI18n } from 'vue-i18n';
+import dayjs from 'dayjs';
+import useLoading from '@/hooks/loading';
+import { Pagination } from '@/types/global';
+import { Modal, Message } from '@arco-design/web-vue';
+import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface';
+import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
+import cloneDeep from 'lodash/cloneDeep';
+import Sortable from 'sortablejs';
+import { numberFormat, amountFormat } from '@/utils/util';
+import {
+    ApiAccountParams, ApiAccountRecord, queryAccountList, UpdateAccount, CreateAccount
+} from '@/api/apiAccount';
+import {
+    TkPoolRecord,
+} from '@/api/taobao';
+import selectAccount from './components/apiSelectAccount.vue';
+
+
+type SizeProps = 'mini' | 'small' | 'medium' | 'large';
+type Column = TableColumnData & { checked?: true };
+
+
+const generateFormModel = () => {
+    return {
+        keyword: '',
+        lastTime: [],
+        status: '',
+        sort: 'status',
+        order: 'descending',
+    };
+};
+const router = useRouter();
+
+const { loading, setLoading } = useLoading(true);
+const { t } = useI18n();
+const renderData = ref<ApiAccountRecord[]>([]);
+const formModel = ref(generateFormModel());
+const cloneColumns = ref<Column[]>([]);
+const showColumns = ref<Column[]>([]);
+
+const size = ref<SizeProps>('small');
+
+const basePagination: Pagination = {
+    current: 1,
+    pageSize: 50,
+};
+const pagination = reactive({
+    ...basePagination,
+});
+const densityList = computed(() => [
+    {
+        name: t('searchTable.size.mini'),
+        value: 'mini',
+    },
+    {
+        name: t('searchTable.size.small'),
+        value: 'small',
+    },
+    {
+        name: t('searchTable.size.medium'),
+        value: 'medium',
+    },
+    {
+        name: t('searchTable.size.large'),
+        value: 'large',
+    },
+]);
+
+const columns = computed<TableColumnData[]>(() => [
+    {
+        title: '#',
+        dataIndex: 'index',
+        slotName: 'index',
+        width: 50,
+    },
+    {
+        title: '账号名',
+        slotName: 'name',
+        width: 250,
+    },
+    {
+        title: 'API KEY',
+        slotName: 'api_key',
+        width: 250,
+    },
+    {
+        title: '账户类型',
+        slotName: 'enable_report',
+        width: 100,
+    },
+    {
+        title: '状态',
+        slotName: 'status',
+        width: 100,
+    },
+    {
+        title: '时间',
+        slotName: 'time',
+        width: 170,
+    },
+    {
+        title: '操作',
+        slotName: 'command',
+        width: 170,
+    },
+]);
+
+const statusOptions = computed<SelectOptionData[]>(() => [
+    {
+        label: '全部',
+        value: '',
+    },
+    {
+        label: '正常',
+        value: '1',
+    },
+    {
+        label: '禁用',
+        value: '0',
+    },
+]);
+
+const fetchData = async (
+    params: ApiAccountParams = {
+        current: 1,
+        pageSize: 50,
+        sort: 'status',
+        order: 'descending',
+        getTotal: true,
+    }
+) => {
+    setLoading(true);
+    try {
+        const { data } = await queryAccountList(params);
+        if (!data) return;
+        renderData.value = data.list;
+        pagination.current = data.page;
+        pagination.total = data.count;
+    } catch (err) {
+        console.log(err);
+    } finally {
+        setLoading(false);
+    }
+};
+
+
+const search = () => {
+    fetchData({
+        ...basePagination,
+        ...formModel.value,
+    } as unknown as ApiAccountParams);
+};
+const onPageChange = (current: number) => {
+    fetchData({ ...basePagination, current });
+};
+
+const visible = ref(false);
+const isEdit = ref(false);
+const editForm = ref<Partial<ApiAccountRecord>>({
+    name: '',
+    description: '',
+    api_key: '',
+    api_secret: '',
+    report_account_ids: '',
+    report_exclude_account_ids: '',
+    status: true,
+    enable_report: true,
+    create_time: new Date(),
+    last_time: new Date()
+});
+
+const onEdit = (current: ApiAccountRecord) => {
+    isEdit.value = true;
+    editForm.value = {
+        ...current,
+        status: Boolean(current.status),
+        enable_report: Boolean(current.enable_report),
+        report_account_ids: current.report_account_ids || '',
+        report_exclude_account_ids: current.report_exclude_account_ids || ''
+    };
+    visible.value = true;
+};
+
+const onAdd = () => {
+    isEdit.value = false;
+    editForm.value = {
+        name: '',
+        description: '',
+        api_key: '',
+        api_secret: '',
+        report_account_ids: '',
+        report_exclude_account_ids: '',
+        status: true,
+        enable_report: true,
+        create_time: new Date(),
+        last_time: new Date()
+    };
+    visible.value = true;
+};
+
+const onDelete = (current: ApiAccountRecord) => {
+    Message.error('为了保证数据安全,禁止删除');
+};
+
+fetchData();
+const reset = () => {
+    formModel.value = generateFormModel();
+};
+
+const handleSelectDensity = (
+    val: string | number | Record<string, any> | undefined,
+    e: Event
+) => {
+    size.value = val as SizeProps;
+};
+
+const handleChange = (
+    checked: boolean | (string | boolean | number)[],
+    column: Column,
+    index: number
+) => {
+    if (!checked) {
+        cloneColumns.value = showColumns.value.filter(
+            (item) => item.dataIndex !== column.dataIndex
+        );
+    } else {
+        cloneColumns.value.splice(index, 0, column);
+    }
+};
+
+const exchangeArray = <T extends Array<any>>(
+    array: T,
+    beforeIdx: number,
+    newIdx: number,
+    isDeep = false
+): T => {
+    const newArray = isDeep ? cloneDeep(array) : array;
+    if (beforeIdx > -1 && newIdx > -1) {
+        // 先替换后面的,然后拿到替换的结果替换前面的
+        newArray.splice(
+            beforeIdx,
+            1,
+            newArray.splice(newIdx, 1, newArray[beforeIdx]).pop()
+        );
+    }
+    return newArray;
+};
+
+const popupVisibleChange = (val: boolean) => {
+    if (val) {
+        nextTick(() => {
+            const el = document.getElementById(
+                'tableSetting'
+            ) as HTMLElement;
+            const sortable = new Sortable(el, {
+                onEnd(e: any) {
+                    const { oldIndex, newIndex } = e;
+                    exchangeArray(cloneColumns.value, oldIndex, newIndex);
+                    exchangeArray(showColumns.value, oldIndex, newIndex);
+                },
+            });
+        });
+    }
+};
+
+const handleSubmit = async () => {
+    try {
+        let response;
+        if (isEdit.value) {
+            // 编辑时调用更新接口
+            response = await UpdateAccount(editForm.value as ApiAccountRecord);
+        } else {
+            // 新增时调用创建接口
+            response = await CreateAccount(editForm.value);
+        }
+
+        if (response.data.success) {
+            Message.success(response.data.msg || '操作成功');
+            visible.value = false;
+            fetchData();
+        } else {
+            Message.error(response.data.msg || '操作失败');
+        }
+    } catch (error) {
+        Message.error('请求失败');
+    }
+};
+
+const handleCancel = () => {
+    visible.value = false;
+};
+
+watch(
+    () => columns.value,
+    (val) => {
+        cloneColumns.value = cloneDeep(val);
+        cloneColumns.value.forEach((item, index) => {
+            item.checked = true;
+        });
+        showColumns.value = cloneDeep(cloneColumns.value);
+    },
+    { deep: true, immediate: true }
+);
+
+</script>
+
+<script lang="ts">
+export default {
+    name: 'SearchTable',
+};
+</script>
+
+<style scoped lang="less">
+.container {
+    padding: 0 20px 20px 20px;
+}
+
+:deep(.arco-table-th) {
+    &:last-child {
+        .arco-table-th-item-title {
+            margin-left: 16px;
+        }
+    }
+}
+
+.action-icon {
+    margin-left: 12px;
+    cursor: pointer;
+}
+
+.active {
+    color: #0960bd;
+    background-color: #e3f4fc;
+}
+
+.setting {
+    display: flex;
+    align-items: center;
+    width: 200px;
+
+    .title {
+        margin-left: 12px;
+        cursor: pointer;
+    }
+}
+</style>

+ 95 - 0
src/views/settings/components/apiSelectAccount.vue

@@ -0,0 +1,95 @@
+<template>
+
+
+    <a-select v-model="model" multiple placeholder="指定账号名" allow-clear @change="changeCompany" :loading="loading">
+        <a-option value="all">不指定账号</a-option>
+        <a-option v-for="item of accounts" :key="item.id" :value="item.id + ''" :label="`${item.id} - ${item.company}`" />
+    </a-select>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, defineEmits, ref, computed, defineProps, PropType } from 'vue';
+import {
+    TkPoolParams,
+    queryTkPool,
+    TkPoolRecord,
+} from '@/api/taobao';
+
+const emits = defineEmits(['update:modelValue', 'change'])
+
+const props = defineProps({
+    modelValue: {
+        type: String,
+        default: ''
+    }
+})
+
+
+
+const model = computed({
+    get: () => {
+        const value = props.modelValue ? props.modelValue.split(',') : ['all'];
+        return value.length === 0 ? ['all'] : value;
+    },
+    set: (value: string[]) => emits('update:modelValue', value.join(','))
+})
+
+const loading = ref(true);
+const accounts = ref<TkPoolRecord[]>([]);
+
+const loadAccounts = async (
+    params: TkPoolParams = {
+        current: 1,
+        pageSize: 9999,
+        sort: 'status',
+        order: 'descending',
+        getTotal: false,
+    }
+) => {
+    loading.value = true;
+    try {
+        const { data } = await queryTkPool(params);
+        accounts.value = data?.list?.filter(item =>
+            item.enable_fake_click === true
+        ) || [];
+    } catch (err) {
+        console.log(err);
+        accounts.value = [];
+    } finally {
+        loading.value = false;
+    }
+};
+
+
+const changeCompany = (value: string[]) => {
+    console.log('change value:', value);
+    // 处理"所有账号"的互斥逻辑
+    let finalValue: string[];
+
+    // 如果当前选择包含 'all'
+    if (value.includes('all')) {
+        // 如果之前没有选中 'all',现在选中了,则只保留 'all'
+        if (!model.value.includes('all')) {
+            finalValue = ['all'];
+        } else {
+            // 如果之前有 'all',现在点击了其他选项,则移除 'all' 并保留新选择的项
+            finalValue = value.filter(v => v !== 'all');
+            if (finalValue.length === 0) {
+                finalValue = ['all'];
+            }
+        }
+    } else {
+        // 如果当前选择不包含 'all'
+        finalValue = value.length === 0 ? ['all'] : value;
+    }
+
+    console.log('final value:', finalValue);
+    emits('update:modelValue', finalValue.join(','));
+    emits('change', finalValue.join(','));
+}
+
+// 使用 onMounted 确保组件挂载后再加载数据
+onMounted(() => {
+    loadAccounts();
+});
+</script>

+ 8 - 2
src/views/settings/config.vue

@@ -116,6 +116,12 @@
                                         <a-input-number v-model="formData.tk_limit_per_oaid_24h" :min="0"
                                             placeholder="同 oaid 在24小时内可转链的次数。" />
                                     </a-form-item>
+
+                                    <a-form-item field="tk_parse_retry_count" label="转链失败重试"
+                                        :rules="[{ required: true }]">
+                                        <a-input-number v-model="formData.tk_parse_retry_count" :min="0"
+                                            placeholder="转链失败重试次数" />
+                                    </a-form-item>
                                 </a-tab-pane>
                                 <a-tab-pane key="jd" title="京东">
                                     <a-form-item field="jdIgnorePercentage" label="放弃流量" :rules="[{ required: true }]">
@@ -321,7 +327,7 @@ const formData = ref<UpdateConfigModel>({
     jdIgnorePercentageCity: '',
     jd_limit_per_ip_24h: 0,
     jd_blacklist_regular: '',
-    
+
     jd_limit_per_oaid_24h: 0,
     jd_h5st_sign_url: '',
     jd_h5st_sign_version: '',
@@ -343,7 +349,7 @@ const formData = ref<UpdateConfigModel>({
     ks_blacklist_regular: '',
     ksTestToken: '',
     ks_limit_per_oaid_24h: 0
-    
+
 });
 
 const showRegularTester = ref<boolean>(false);