feat(card-walmart-c-trip): 实现沃尔玛 Cookie 管理功能

- 新增沃尔玛 Cookie管理页面,包括搜索、添加、编辑、删除等功能
- 实现批量导入和导出功能
- 添加账户详情和操作日志组件
- 集成通知和表单验证功能
This commit is contained in:
danial
2025-03-06 23:40:44 +08:00
parent cbf0c5e5fb
commit 92679b841b
9 changed files with 1827 additions and 1 deletions

View File

@@ -0,0 +1,219 @@
<template>
<a-modal
:visible="props.visible"
:ok-loading="loading"
draggable
@cancel="handleCancel"
>
<template #title>{{ props.id === '' ? '新增' : '编辑' }}Cookie</template>
<a-form ref="formDataRef" :model="formData">
<a-form-item field="name" label="名称" required>
<a-input v-model="formData.name" />
</a-form-item>
<a-form-item
field="cookie"
label="cookie"
:disabled="props.id !== ''"
required
>
<a-space
direction="vertical"
fill
style="
width: 100%;
width: -moz-available;
width: -webkit-fill-available;
"
>
<a-textarea
v-model="formData.cookie"
:autoSize="{ minRows: 3, maxRows: 10 }"
/>
</a-space>
</a-form-item>
<a-form-item
field="maxAmountLimit"
label="充值限制(金额)"
tooltip="0表示没有限制"
required
>
<a-input-number v-model="formData.maxAmountLimit" />
</a-form-item>
<a-form-item
field="maxCountLimit"
label="充值限制(次数)"
tooltip="0表示没有限制"
required
>
<a-input-number v-model="formData.maxCountLimit" />
</a-form-item>
<a-form-item
field="status"
label="状态"
:disabled="![0, 1].includes(formData.status)"
>
<a-switch
v-model="formData.status"
:checked-value="1"
:unchecked-value="0"
/>
</a-form-item>
<a-form-item field="remark" label="备注">
<a-textarea v-model="formData.remark" />
</a-form-item>
</a-form>
<template #footer>
<!-- <a-button
v-if="props.id === ''"
type="secondary"
status="warning"
:loading="renderData.loading"
@click="checkCookie"
>
检测Cookie
</a-button> -->
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="loading" @click="handleOk">
确定
</a-button>
<!-- <a-button
type="primary"
:loading="loading"
:disabled="!renderData.cookieStatus && props.id === ''"
@click="handleOk"
>
确定
</a-button> -->
</a-space>
</template>
</a-modal>
</template>
<script lang="ts" setup>
import useLoading from '@/hooks/loading';
import { FormInstance } from '@arco-design/web-vue';
import { ref, PropType, watch, reactive } from 'vue';
import { isNull } from '@/utils/is.ts';
import { notification } from './component.tsx';
import {
type KamiApiCardInfoCTripV1AccountCreateReq,
KamiApiCardInfoWalmartV1AccountListRecord
} from '@/api/generated/index.ts';
import { apiClient } from '@/api';
const props = defineProps({
visible: {
type: Boolean,
default: false
},
id: {
type: String,
default: ''
},
account: {
type: Object as PropType<
Required<KamiApiCardInfoWalmartV1AccountListRecord>
>,
default: null
}
});
const generateFormData = () => {
return {
status: 1,
maxAmountLimit: 0,
maxCountLimit: 0,
cookie: '',
remark: '',
name: ''
};
};
const formData =
ref<KamiApiCardInfoCTripV1AccountCreateReq>(generateFormData());
const formDataRef = ref<FormInstance>();
const { loading, setLoading } = useLoading(false);
const renderData = reactive<{ cookieStatus: boolean; loading: boolean }>({
cookieStatus: false,
loading: false
});
const emit = defineEmits(['update:visible']);
const handleOk = () => {
if (formData.value) {
formDataRef.value.validate().then(async res => {
if (res) return;
setLoading(true);
try {
if (props.id === '') {
await apiClient.apiCardInfoCTripAccountCreatePost(formData.value);
} else {
await apiClient.apiCardInfoCTripAccountUpdatePut({
id: props.id,
...formData.value
});
}
emit('update:visible', false);
formData.value = generateFormData();
} catch (error) {
} finally {
setLoading(false);
}
});
}
};
watch(
() => props.account,
val => {
if (!isNull(val)) {
formData.value = { ...val };
}
}
);
const handleCancel = () => {
emit('update:visible', false);
formData.value = generateFormData();
};
watch(
() => formData.value.cookie,
val => {
renderData.cookieStatus = false;
}
);
const checkCookie = async () => {
renderData.loading = false;
try {
const cookieResp = await apiClient.apiCardInfoCTripAccountCheckCookiePost({
cookie: formData.value.cookie.trim()
});
if (cookieResp.data.isAvailable) {
notification(
true,
cookieResp.data.nickname,
cookieResp.data.balance,
cookieResp.data.isExist
);
renderData.cookieStatus = true;
if (props.id === '') {
renderData.cookieStatus = !cookieResp.data.isExist;
}
} else {
notification(false);
}
} finally {
renderData.loading = false;
}
};
</script>
<style lang="less" scoped>
.footer {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,257 @@
import { getAPIBaseUrl, handleDownLoadFile } from '@/api/utils';
import useLoading from '@/hooks/loading';
import { getToken, getTokenFrom } from '@/utils/auth';
import {
Button,
Col,
type FileItem,
Message,
Modal,
Notification,
Row,
Space,
Table,
type TableColumnData,
type TableData,
Tag,
Upload
} from '@arco-design/web-vue';
import { isArray } from 'lodash';
import { defineComponent, reactive, ref } from 'vue';
import type { KamiApiCardInfoCTripV1AccountCookieCheckRes } from '@/api/generated';
import { apiClient } from '@/api';
const tableColumns: TableColumnData[] = [
{
title: '序号',
dataIndex: 'index',
render: (data: { rowIndex: number }) => {
return data.rowIndex + 1;
}
},
{
title: '名称',
dataIndex: 'name'
},
{
title: '昵称',
dataIndex: 'nickname'
},
{
title: '限额(金额)',
dataIndex: 'maxAmountLimit'
},
{
title: '限额(次数)',
dataIndex: 'maxCountLimit'
},
{
title: 'cookie',
dataIndex: 'cookie',
ellipsis: true,
tooltip: true
},
{
title: '是否存在系统内',
dataIndex: 'isExist',
render: (data: { record: TableData }) => {
let color = 'red';
let text = '不存在';
if (data.record.isExist) {
color = 'green';
text = '存在';
}
return (
<Tag size='small' color={color}>
{text}
</Tag>
);
}
},
{
title: '是否可用',
dataIndex: 'isAvailable',
render: (data: { record: TableData }) => {
let color = 'red';
let text = '不可用';
if (data.record.isAvailable) {
color = 'green';
text = '可用';
}
return (
<Tag size='small' color={color}>
{text}
</Tag>
);
}
}
];
export const notification = (
isSucceed: boolean = true,
nickname: string = '',
balance: number = 0,
isExist: boolean = false
) => {
if (isSucceed) {
const id = `${Date.now()}`;
Notification.success({
id,
title: '用户信息',
content: () => (
<div>
<div>{nickname}</div>
<div>{balance}</div>
<div>{isExist ? '存在' : '不存在'}</div>
</div>
),
closable: true,
footer: () => (
<Space>
<Button
status='normal'
type='primary'
onClick={() => Notification.remove(id)}
>
</Button>
</Space>
),
duration: 0
});
} else {
const id = `${Date.now()}`;
Notification.error({
id,
title: '用户信息',
content: 'cookie无效',
footer: () => (
<Space>
<Button
status='danger'
type='primary'
onClick={() => Notification.remove(id)}
>
</Button>
</Space>
),
duration: 0
});
}
};
// 新增下载模板文件的函数
const downloadDataList = async () => {
const response = await apiClient.apiCardInfoCTripAccountDownloadGet({
responseType: 'blob'
});
handleDownLoadFile(response.data as any, '沃尔玛账号下载数据.xlsx');
};
export const batchImportModel = defineComponent({
name: 'batchImportWalmartAccountModel',
emits: {
close: () => {}
},
setup(props, { emit }) {
const state = reactive<{
visible: boolean;
}>({
visible: false
});
const { loading, setLoading } = useLoading(false);
const tableData = ref<KamiApiCardInfoCTripV1AccountCookieCheckRes[]>([]);
return () => (
<>
<Button
type='primary'
size='small'
onClick={() => (state.visible = !state.visible)}
>
</Button>
<Modal
title='批量导入'
width='80%'
v-model:visible={state.visible}
onBeforeOk={async (done: (closed: boolean) => void) => {
try {
await apiClient.apiCardInfoCTripAccountBatchAddPost({
list: tableData.value
});
emit('close');
Message.success('上传成功');
done(true);
} catch {
Message.error('上传失败');
done(false);
}
}}
>
<Row gutter={20}>
<Col span={4}>
<Space direction='vertical' fill>
<Button
long
status='warning'
onClick={() => downloadDataList()}
>
</Button>
<Upload
disabled={loading.value}
onBeforeUpload={async (
file: File
): Promise<boolean | File> => {
setLoading(true);
tableData.value = [];
return file;
}}
draggable
limit={1}
accept='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
onSuccess={(fileItem: FileItem) => {
setLoading(false);
if (fileItem.response.code === 0) {
if (
isArray(fileItem.response.data.list) &&
fileItem.response.data.list.length > 0
) {
tableData.value = fileItem.response.data.list;
}
Message.success(
`上传成功${fileItem.response.data?.msg ? '' + fileItem.response.data?.msg : ''}`
);
return;
}
Message.error(`上传失败:${fileItem.response.message}`);
}}
onError={() => {
Message.error('上传失败');
setLoading(false);
}}
action={`${getAPIBaseUrl()}/api/cardInfo/cTrip/account/check`}
headers={{
Authorization: `Bearer ${getToken()}`,
tokenFrom: getTokenFrom()
}}
tip='点击批量上传ck'
/>
</Space>
</Col>
<Col span={20}>
<Table
pagination={false}
loading={loading.value}
data={tableData.value}
columns={tableColumns}
></Table>
</Col>
</Row>
</Modal>
</>
);
}
});

View File

@@ -0,0 +1,162 @@
<template>
<a-drawer
:visible="drawVisible"
width="70%"
:footer="false"
unmountOnClose
@open="openDrawer"
@cancel="closeDrawer"
>
<template #title>账户流水</template>
<a-table
row-key="id"
:loading="loading"
:pagination="{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
pageSizeOptions: [10, 20, 50, 100],
showPageSize: true
}"
:columns="columns"
:data="renderData"
:bordered="false"
size="small"
column-resizable:bordered="{cell:true}"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</a-drawer>
</template>
<script lang="ts" setup>
import useLoading from '@/hooks/loading';
import { Pagination } from '@/types/global';
import { computed, onMounted, reactive, ref } from 'vue';
import { TableColumnData } from '@arco-design/web-vue';
import {
ApiCardInfoCTripAccountGetWalletListGetPageSizeEnum,
KamiInternalModelEntityV1CardRedeemAccountHistory
} from '@/api/generated';
import { apiClient } from '@/api';
const basePagination: Pagination = {
current: 1,
pageSize: 50
};
const pagination = reactive({
...basePagination
});
const columns: TableColumnData[] = [
{
title: '序号',
slotName: 'index',
render: ({ rowIndex }) => {
return rowIndex + 1 + (pagination.current - 1) * pagination.pageSize;
}
},
{
title: '订单号',
dataIndex: 'orderNo'
},
{
title: '金额',
dataIndex: 'amount',
render: ({ record }) => {
switch (record.operationStatus) {
case 'return':
return `-${record.amount}`;
case 'deduct':
return `+${record.amount}`;
case 'initialize':
return `+${record.amount}`;
default:
return '未知';
}
}
},
{
title: '操作状态',
dataIndex: 'operationStatus',
render: ({ record }) => {
switch (record.operationStatus) {
case 'return':
return '退款';
case 'deduct':
return '加款';
case 'initialize':
return '初始化';
default:
return '未知';
}
}
},
{
title: '备注',
dataIndex: 'remark'
},
{
title: '创建时间',
dataIndex: 'createdAt',
slotName: 'createdAt'
}
];
const { loading, setLoading } = useLoading(true);
const renderData = ref<KamiInternalModelEntityV1CardRedeemAccountHistory[]>([]);
const props = defineProps<{
visible: boolean;
accountId: string;
}>();
const state = reactive({
visible: false
});
const emits = defineEmits<{
(e: 'update:visible', visible: boolean): void;
}>();
onMounted(() => {
state.visible = props.visible;
});
const drawVisible = computed(() => props.visible);
const fetchData = async (
params: Pagination = {
current: 1,
pageSize: 50
}
) => {
setLoading(true);
try {
const {
data: { list, total }
} = await apiClient.apiCardInfoCTripAccountGetWalletListGet(
params.current,
params.pageSize as ApiCardInfoCTripAccountGetWalletListGetPageSizeEnum,
props.accountId
);
renderData.value = list;
pagination.current = params.current;
pagination.pageSize = params.pageSize;
pagination.total = total;
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
const onPageChange = (current: number) => {
fetchData({ ...pagination, current });
};
const onPageSizeChange = (pageSize: number) => {
fetchData({ ...pagination, pageSize });
};
const closeDrawer = () => {
emits('update:visible', false);
};
const openDrawer = () => {
fetchData();
};
</script>

View File

@@ -0,0 +1,416 @@
<template>
<div class="container">
<Breadcrumb :items="['充值账户管理', '沃尔玛Cookie']" />
<a-card class="general-card" title="沃尔玛Cookie管理">
<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="cookie" label="cookie">
<a-input
v-model="formModel.cookie"
placeholder="请输入Cookie"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item field="name" label="名称">
<a-input v-model="formModel.name" placeholder="请输入名称" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item field="nickname" label="沃尔玛昵称">
<a-input
v-model="formModel.nickname"
placeholder="请输入沃尔玛账户昵称"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-col>
<a-divider style="height: 42px" direction="vertical" />
<a-col flex="172px" style="text-align: right">
<a-space direction="horizontal" :size="18">
<a-button type="primary" @click="search">
<template #icon>
<icon-search />
</template>
搜索
</a-button>
<a-button @click="reset">
<template #icon>
<icon-refresh />
</template>
重置
</a-button>
</a-space>
</a-col>
</a-row>
<a-divider style="margin-top: 0" />
<a-row style="margin-bottom: 16px">
<a-space>
<a-button
v-if="checkTokenFromLogin()"
type="primary"
size="small"
@click="showAddModel({ id: '', ...generateFormModel() })"
>
<template #icon>
<icon-plus />
</template>
添加
</a-button>
<a-button @click="download">
<template #icon>
<icon-download />
</template>
导出
</a-button>
<batchImportModel @close="() => search()" />
</a-space>
</a-row>
<a-table
:loading="loading"
:pagination="{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
pageSizeOptions: [10, 20, 50, 100],
showPageSize: true
}"
:columns="columns"
:data="renderData"
:scroll="{ x: 1080 }"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
>
<template #status="{ record }">
<a-space size="small">
<a-switch
v-if="[0, 1].includes(record.status)"
v-model="record.status"
size="small"
:checked-value="1"
:unchecked-value="0"
:beforeChange="newValue => updateCurrentStatus(record, newValue)"
/>
<a-tag
v-else
size="small"
:color="statusMapper(record.status).color"
>
{{ statusMapper(record.status).text }}
</a-tag>
</a-space>
</template>
<template #operations="{ record }">
<a-space size="small">
<a-tooltip content="修改">
<a-button
size="small"
status="warning"
@click="showAddModel(record)"
>
<template #icon>
<icon-pen />
</template>
</a-button>
</a-tooltip>
<a-tooltip content="详情">
<a-button size="small" @click="showDetailModel(record)">
<template #icon>
<icon-list />
</template>
</a-button>
</a-tooltip>
<a-popconfirm
type="warning"
content="确认刷新账号状态嘛?"
@ok="refreshButton(record)"
>
<a-tooltip content="刷新">
<a-button
v-if="record.status === 3"
size="small"
status="success"
>
<template #icon>
<icon-refresh />
</template>
</a-button>
</a-tooltip>
</a-popconfirm>
<a-tooltip content="删除">
<a-button
v-if="checkTokenFromIframe()"
size="small"
status="danger"
@click="deleteOne(record.id)"
>
<template #icon>
<icon-delete />
</template>
</a-button>
</a-tooltip>
</a-space>
</template>
</a-table>
</a-card>
<add-modal
:id="state.accountId"
v-model:visible="state.addModalVisible"
:account="state.account"
/>
<account-detail
v-model:visible="state.accountDetailVisible"
:accountId="state.accountId"
/>
</div>
</template>
<script lang="ts" setup>
import useLoading from '@/hooks/loading';
import { Pagination } from '@/types/global';
import { checkTokenFromIframe, checkTokenFromLogin } from '@/utils/auth';
import { onMounted, reactive, ref, watchEffect } from 'vue';
import { Notification, TableColumnData } from '@arco-design/web-vue';
import AddModal from './components/add-modal.vue';
import AccountDetail from './components/detail.vue';
import { batchImportModel } from './components/component.tsx';
import { apiClient } from '@/api';
import {
ApiCardInfoCTripOrderListGetPageSizeEnum,
KamiApiCardInfoCTripV1AccountListRecord
} from '@/api/generated';
import { handleDownLoadFile } from '@/api/utils.ts';
const basePagination: Pagination = {
current: 1,
pageSize: 50
};
const pagination = reactive({
...basePagination
});
const columns: TableColumnData[] = [
{
title: '序号',
dataIndex: 'index',
render: ({ rowIndex }) => {
return rowIndex + 1 + (pagination.current - 1) * pagination.pageSize;
}
},
{
title: '名称',
dataIndex: 'name'
},
{
title: '昵称',
dataIndex: 'nickname'
},
{
title: '余额',
dataIndex: 'balance'
},
{
title: '今日充值金额',
dataIndex: 'amountTodaySum'
},
{
title: '今日充值次数',
dataIndex: 'amountTodayCount'
},
{
title: '充值限制(金额)',
dataIndex: 'maxAmountLimit'
},
{
title: '充值限制(次数)',
dataIndex: 'maxCountLimit'
},
{
title: '累计充值金额',
dataIndex: 'amountTotalSum'
},
{
title: '累计充值次数',
dataIndex: 'amountTotalCount'
},
{
title: 'ck',
dataIndex: 'cookie',
ellipsis: true,
tooltip: true
},
{
title: '创建人',
dataIndex: 'uploadUser.username'
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status'
},
{
title: '创建时间',
dataIndex: 'createdAt',
slotName: 'createdAt'
},
{
title: '操作',
dataIndex: 'operations',
slotName: 'operations',
fixed: 'right',
width: 220
}
];
const generateFormModel = () => {
return {
remark: '',
name: '',
cookie: '',
nickname: '',
maxAmountLimit: 0,
status: 1
};
};
interface formDataType {
remark: string;
name: string;
cookie: string;
nickname: string;
maxAmountLimit: number;
status: number;
}
const { loading, setLoading } = useLoading(true);
const renderData = ref<KamiApiCardInfoCTripV1AccountListRecord[]>([]);
const formModel = ref<formDataType>(generateFormModel());
const state = reactive({
addModalVisible: false,
deployModalVisible: false,
accountDetailVisible: false,
accountId: '',
account: null
});
const fetchData = async (
page: Pagination = { current: 1, pageSize: 50 },
formData: Partial<formDataType> = {}
) => {
setLoading(true);
try {
const {
data: { list, total }
} = await apiClient.apiCardInfoCTripOrderListGet(
page.current,
page.pageSize as ApiCardInfoCTripOrderListGetPageSizeEnum
);
renderData.value = list;
pagination.current = page.current;
pagination.pageSize = page.pageSize;
pagination.total = total;
} catch (err) {
} finally {
setLoading(false);
}
};
const onPageChange = (current: number) => {
fetchData({ ...pagination, current });
};
const onPageSizeChange = (pageSize: number) => {
fetchData({ ...pagination, pageSize });
};
const search = () => {
fetchData(basePagination, formModel.value);
};
const reset = () => {
formModel.value = generateFormModel();
};
const deleteOne = async (id: string) => {
try {
await apiClient.apiCardInfoCTripAccountDeleteDelete(id);
} catch {
Notification.error({
id: 'walmartAccountNotice',
content: '删除沃尔玛卡失败',
closable: true
});
} finally {
await fetchData({ ...pagination });
}
};
const showAddModel = (record: KamiApiCardInfoCTripV1AccountListRecord) => {
state.addModalVisible = true;
state.accountId = record.id;
state.account = record;
};
const showDetailModel = (record: KamiApiCardInfoCTripV1AccountListRecord) => {
state.accountDetailVisible = true;
state.accountId = record.id;
};
watchEffect(() => {
// 目标账户和存储用户不能相同
if (!state.addModalVisible) {
search();
}
});
const updateCurrentStatus = async (
record: KamiApiCardInfoCTripV1AccountListRecord,
newValue: string | number | boolean
) => {
await apiClient.apiCardInfoCTripAccountUpdateStatusPut({
id: record.id,
status: newValue as number
});
record.status = newValue as number;
};
const statusMapper = (status: number): { text: string; color: string } => {
switch (status) {
case 0:
return { text: '失效', color: 'red' };
case 1:
return { text: '正常', color: 'green' };
case 2:
return { text: '充值过快', color: 'orange' };
case 3:
return { text: '充值限制(用户设置)', color: 'red' };
case 4:
return { text: '充值限制(其他)', color: 'red' };
case 5:
return { text: '充值限制(低额)', color: 'red' };
case 6:
return { text: '充值限制(安全原因)', color: 'red' };
case 7:
return { text: '充值限制(暂时限制)', color: 'red' };
case 8:
return { text: '充值限制(平台原因)', color: 'red' };
default:
return { text: '未知', color: 'gray' };
}
};
const refreshButton = (record: KamiApiCardInfoCTripV1AccountListRecord) => {
apiClient
.apiCardInfoCTripAccountRefreshStatusPut({ id: record.id })
.then(() => {
fetchData();
});
};
onMounted(() => {
fetchData();
});
const download = () => {
apiClient.apiCardInfoCTripAccountDownloadGet().then(res => {
handleDownLoadFile(res.data as any, '沃尔玛卡列表.xlsx');
});
};
</script>

View File

@@ -0,0 +1,129 @@
<template>
<div class="container">
<Breadcrumb :items="['订单', '订单汇总']" />
<a-card class="general-card" title="订单汇总">
<a-table
:columns="columns"
:loading="loading"
:data="renderData"
:pagination="{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
pageSizeOptions: [10, 20, 50, 100],
showPageSize: true
}"
:bordered="false"
column-resizable:bordered="{cell:true}"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
>
<template #index="{ rowIndex }">
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
</template>
</a-table>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { Pagination } from '@/types/global';
import { onMounted, reactive, ref } from 'vue';
import { OrderSummaryRecord, queryOrderSummaryList } from '@/api/order-summary';
import useLoading from '@/hooks/loading';
import { TableColumnData } from '@arco-design/web-vue/es/table';
import { useRoute } from 'vue-router';
const route = useRoute();
const basePagination: Pagination = {
current: 1,
pageSize: 50
};
const columns: TableColumnData[] = [
{
title: '序号',
dataIndex: 'index',
slotName: 'index'
},
{
title: '商户ID',
dataIndex: 'merchantUid'
},
{
title: '商户名称',
dataIndex: 'merchantName'
},
{
title: '成交金额(面额)',
dataIndex: 'succeedShowAmount'
},
{
title: '成交金额(实际)',
dataIndex: 'succeedFactAmount'
},
{
title: '成交数量',
dataIndex: 'succeedCount'
},
{
title: '总额(面额)',
dataIndex: 'totalShowAmount'
},
{
title: '总额(实际)',
dataIndex: 'totalFactAmount'
},
{
title: '订单总数',
dataIndex: 'totalCount'
},
{
title: '失败数量',
dataIndex: 'failedCount'
},
{
title: '失败(未填写)订单数量',
dataIndex: 'waitedCount'
},
{
title: '成功率',
dataIndex: 'rate'
},
{
title: '日期',
dataIndex: 'date'
}
];
const { loading, setLoading } = useLoading(true);
const pagination = reactive({
...basePagination
});
const renderData = ref<OrderSummaryRecord[]>([]);
const fetchData = async (
params: { current: number; pageSize: number } = basePagination
) => {
setLoading(true);
const {
data: { list, total }
} = await queryOrderSummaryList({
...params,
roadUid: route.params.roadUid as string
});
console.log(params);
renderData.value = list;
pagination.current = params.current;
pagination.pageSize = params.pageSize;
pagination.total = total;
setLoading(false);
};
const onPageChange = (current: number) => {
fetchData({ ...pagination, current });
};
const onPageSizeChange = (pageSize: number) => {
fetchData({ ...pagination, pageSize });
};
onMounted(() => {
fetchData();
});
</script>

View File

@@ -0,0 +1,113 @@
import { apiClient } from '@/api';
import { getWalmartConfig } from '@/api/card-walmart-order';
import type { KamiApiCardInfoWalmartV1RedeemConfigGetRes } from '@/api/generated';
import {
Button,
Modal,
Form,
FormItem,
Switch,
type FormInstance,
InputNumber
} from '@arco-design/web-vue';
import { defineComponent, onMounted, reactive, ref } from 'vue';
export default defineComponent({
name: 'cardJdConfig',
setup() {
const state = reactive<{
visible: boolean;
}>({
visible: false
});
const formData = ref<Required<KamiApiCardInfoWalmartV1RedeemConfigGetRes>>({
isAllowDifferentAmount: true,
isAllowDifferentSucceedCallback: true,
isAllowDifferentFailCallback: true,
redeemCardMinAmount: 0,
isAllowCompensatedCallback: true,
redeemCardRate: 100
});
const formRef = ref<FormInstance>();
onMounted(() => {
getWalmartConfig().then(res => {
formData.value = res.data;
});
});
return () => (
<>
<Button
type='primary'
status='normal'
onClick={() => {
state.visible = !state.visible;
getWalmartConfig().then(res => {
formData.value = res.data;
});
}}
>
</Button>
<Modal
title='充值设置'
v-model:visible={state.visible}
onBeforeOk={async (done: (closed: boolean) => void) => {
if (await formRef.value.validate()) {
done(false);
}
try {
await apiClient.apiCardInfoWalmartConfigSetPost(formData.value);
done(true);
} catch {
done(false);
}
}}
>
<Form
ref={formRef}
model={formData}
labelColProps={{ span: 12 }}
wrapperColProps={{ span: 12 }}
>
<FormItem field='redeemCardMinAmount' label='最小充值金额' required>
<InputNumber v-model={formData.value.redeemCardMinAmount} />
</FormItem>
<FormItem
field='isAllowDifferentAmount'
label='是否兑换(金额异议)'
required
>
<Switch v-model={formData.value.isAllowDifferentAmount} />
</FormItem>
<FormItem
field='isAllowDifferentSucceedCallback'
label='成功是否回调(金额异议)'
required
>
<Switch
v-model={formData.value.isAllowDifferentSucceedCallback}
/>
</FormItem>
<FormItem
field='isAllowDifferentFailCallback'
label='失败是否回调(金额异议)'
required
>
<Switch v-model={formData.value.isAllowDifferentFailCallback} />
</FormItem>
<FormItem
field='isAllowCompensatedCallback'
label='补卡自动回调'
required
>
<Switch v-model={formData.value.isAllowCompensatedCallback} />
</FormItem>
<FormItem field='redeemCardRate' label='充值速率(单/分钟)' required>
<InputNumber v-model={formData.value.redeemCardRate} />
</FormItem>
</Form>
</Modal>
</>
);
}
});

View File

@@ -0,0 +1,137 @@
<template>
<a-drawer
v-model:visible="state.visible"
width="40%"
title="充值操作日志"
:footer="false"
unmount-on-close
@close="closeDrawer"
>
<div v-if="props.orderNo !== ''">
<a-radio-group
v-model="state.isReverse"
type="button"
size="small"
class="sort-btn"
>
<a-radio :value="true">正序</a-radio>
<a-radio :value="false">逆序</a-radio>
</a-radio-group>
<a-timeline :reverse="state.isReverse">
<a-timeline-item
v-for="(item, index) in renderData"
:key="index"
:label="item.createdAt"
>
<a-space>
<span>
{{ mapStatus(item.operationStatus) }}
</span>
<span class="remark">
{{ item.remark }}
</span>
</a-space>
</a-timeline-item>
</a-timeline>
</div>
<div v-else>404</div>
</a-drawer>
</template>
<script setup lang="ts">
import { queryWalmartOrderHistoryList } from '@/api/card-walmart-order';
import { KamiInternalModelEntityV1CardRedeemOrderHistory } from '@/api/generated';
import { reactive, ref, watch } from 'vue';
const props = defineProps({
orderNo: {
type: String,
default: ''
}
});
const visible = defineModel<boolean>('visible');
const state = reactive<{ isReverse: boolean; visible: boolean }>({
isReverse: true,
visible: false
});
const renderData = ref<KamiInternalModelEntityV1CardRedeemOrderHistory[]>([]);
const getJDOrderHistoryList = async () => {
const res = await queryWalmartOrderHistoryList({
orderNo: props.orderNo
});
renderData.value = res.data.list;
};
watch(
() => visible.value,
newValue => {
state.visible = newValue;
if (newValue) {
getJDOrderHistoryList();
} else {
renderData.value = [];
}
}
);
const closeDrawer = () => {
visible.value = false;
// emit('update:visible', false);
};
const mapStatus = (opeationStatus: number) => {
switch (opeationStatus) {
case 100:
return '充值失败';
case 1:
return '添加订单';
case 2:
return '分配账号';
case 3:
return '充值成功';
case 4:
return '开始回调';
case 5:
return '订单退回';
case 6:
return '订单失败(账户问题)';
case 7:
return '分配账号失败';
case 8:
return '订单验证失败';
case 9:
return '开始回调';
case 10:
return '回调成功';
case 11:
return '回调失败 ';
case 12:
return '订单未处于充值结束状态';
case 13:
return '用户设置不触发回调';
case 14:
return '未知状态';
case 15:
return '服务器错误';
case 16:
return '开始处理';
default:
return '未知状态';
}
};
</script>
<style lang="less" scoped>
.sort-btn {
float: right;
}
.remark {
font-size: 0.8rem;
font-weight: 300;
}
</style>

View File

@@ -0,0 +1,394 @@
<template>
<div class="container">
<Breadcrumb :items="['充值账户管理', '充值流水']" />
<a-card class="general-card" title="流水记录">
<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="giftCardPwd" label="卡密">
<a-input
v-model="formModel.giftCardPwd"
placeholder="请输入卡密"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item field="attach" label="商户订单号">
<a-input
v-model="formModel.attach"
placeholder="请输入商户订单号"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item field="merchantId" label="系统订单号">
<a-input
v-model="formModel.merchantId"
placeholder="请输入系统订单号"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item field="dateRange" label="创建时间">
<a-range-picker v-model="formModel.dateRange" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item field="accountNickName" label="账户昵称">
<a-input
v-model="formModel.accountNickName"
placeholder="请输入账户昵称"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item field="accountCk" label="账户Ck">
<a-input
v-model="formModel.accountCk"
placeholder="请输入账户Ck"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-col>
<a-divider style="height: 42px" direction="vertical" />
<a-col flex="172px" style="text-align: right">
<a-space direction="vertical" :size="18">
<a-space direction="horizontal" :size="18">
<a-button type="primary" @click="search">
<template #icon>
<icon-search />
</template>
搜索
</a-button>
<a-button @click="reset">
<template #icon>
<icon-refresh />
</template>
重置
</a-button>
</a-space>
</a-space>
</a-col>
</a-row>
<a-divider style="margin-top: 0" />
<a-space direction="vertical" fill>
<config v-if="state.showConfig" />
<a-table
:loading="loading"
:pagination="{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
pageSizeOptions: [10, 20, 50, 100],
showPageSize: true,
showTotal: true
}"
table-layout-fixed
column-resizable
:columns="columns"
:data="renderData"
:bordered="false"
column-resizable:bordered="{cell:true}"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
>
<template #status="{ record }">
<a-tag size="small" :color="statusMapper(record.status).color">
{{ statusMapper(record.status).text }}
</a-tag>
</template>
<template #orderStatus="{ record }">
<a-tag
v-if="record.orderStatus !== 0"
size="small"
:color="statusMapper(record.orderStatus).color"
>
{{ statusMapper(record.orderStatus).text }}
</a-tag>
</template>
<template #notifyStatus="{ record }">
<a-tag
v-if="[1, 2].includes(record.notifyStatus)"
size="small"
:color="record.notifyStatus === 1 ? 'green' : 'red'"
>
回调{{ record.notifyStatus === 1 ? '成功' : '失败' }}
</a-tag>
<a-tag v-else size="small" color="gray">未回调</a-tag>
</template>
<template #operation="{ record }">
<a-space size="small">
<a-tooltip content="详情">
<a-button
size="small"
@click="showOrderHistory(record.orderNo)"
>
<template #icon>
<icon-list />
</template>
</a-button>
</a-tooltip>
<!-- 回调 -->
<a-tooltip content="回调">
<a-button
v-if="
record.notifyStatus !== 1 &&
[1, 100, 17].includes(record.orderStatus)
"
size="small"
status="warning"
@click="callback(record.orderNo)"
>
<template #icon>
<icon-send />
</template>
</a-button>
</a-tooltip>
</a-space>
</template>
</a-table>
</a-space>
</a-card>
<order-history
v-model:visible="state.orderHistoryModalVisible"
:order-no="state.selectedOrderNo"
/>
</div>
</template>
<script lang="ts" setup>
import { TableColumnData, Notification } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading';
import { Pagination } from '@/types/global';
import { onMounted, reactive, ref } from 'vue';
import config from './components/config.tsx';
import OrderHistory from './components/history.vue';
import { checkTokenFromIframe } from '@/utils/auth';
import {
queryWalmartCardOrderList,
walmartCardOrderParams
} from '@/api/card-walmart-order.ts';
import { KamiInternalModelEntityV1CardRedeemOrderInfo } from '@/api/generated/index.ts';
import { apiClient } from '@/api/index.ts';
const basePagination: Pagination = {
current: 1,
pageSize: 50
};
const state = reactive({
orderHistoryModalVisible: false,
selectedOrderNo: '',
showConfig: false
});
const pagination = reactive({
...basePagination
});
const columns: TableColumnData[] = [
{
title: '序号',
dataIndex: 'index',
render: ({ rowIndex }) => {
return rowIndex + 1 + (pagination.current - 1) * pagination.pageSize;
}
},
{
title: '系统订单号',
dataIndex: 'merchantId'
},
{
title: '商户订单号',
dataIndex: 'attach'
},
{
title: '分配账号',
dataIndex: 'accountName'
},
{
title: '卡种',
dataIndex: 'cardTypeName'
},
{
title: '卡号',
dataIndex: 'cardNo',
tooltip: true,
ellipsis: true
},
{
title: '礼品卡密码',
dataIndex: 'giftCardPwd'
},
{
title: '卡面金额',
dataIndex: 'orderAmount'
},
{
title: '实际金额',
dataIndex: 'actualAmount'
},
{
title: '充值状态',
dataIndex: 'status',
slotName: 'status'
},
{
title: '状态明细',
dataIndex: 'orderStatus',
slotName: 'orderStatus'
},
{
title: '回调状态',
dataIndex: 'notifyStatus',
slotName: 'notifyStatus'
},
// {
// title: '备注',
// dataIndex: 'remark'
// },
{
title: '创建时间',
dataIndex: 'createdAt'
},
{
title: '操作',
dataIndex: 'operation',
slotName: 'operation'
}
];
const generateFormModel = () => {
return {
merchantId: '',
attach: '',
giftCardPwd: '',
accountNickName: '',
accountCk: '',
status: null,
dateRange: []
};
};
const { loading, setLoading } = useLoading(true);
const renderData = ref<KamiInternalModelEntityV1CardRedeemOrderInfo[]>([]);
const formModel = ref<{
merchantId: string;
accountNickName: string;
accountCk: string;
attach: string;
giftCardPwd: string;
status: number;
dateRange: Date[];
}>(generateFormModel());
const fetchData = async (
params: walmartCardOrderParams = {
current: 1,
pageSize: 50
}
) => {
setLoading(true);
try {
const {
data: { list, total }
} = await queryWalmartCardOrderList(params);
renderData.value = list;
pagination.current = params.current;
pagination.pageSize = params.pageSize;
pagination.total = total;
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
const onPageChange = (current: number) => {
fetchData({ ...pagination, current });
};
const onPageSizeChange = (pageSize: number) => {
fetchData({ ...pagination, pageSize, current: 1 });
};
const search = () => {
fetchData({
...basePagination,
...formModel.value,
current: 1
});
};
const showOrderHistory = (orderNo: string) => {
state.orderHistoryModalVisible = true;
state.selectedOrderNo = orderNo;
};
const statusMapper = (status: number): { text: string; color: string } => {
switch (status) {
case 100:
return { text: '失败', color: 'red' };
case 1:
return { text: '成功', color: 'green' };
case 2:
return { text: '待处理', color: 'orange' };
case 3:
return { text: '验证失败', color: 'red' };
case 4:
return { text: '账号失效', color: 'red' };
case 5:
return { text: '账号充值频繁', color: 'red' };
case 6:
return { text: '未知', color: 'gray' };
case 7:
return { text: '禁用(充值次数超限)', color: 'red' };
case 8:
return { text: '金额异议(充值成功)', color: 'pinkpurple' };
case 9:
return { text: '卡密类型错误', color: 'red' };
case 10:
return { text: '金额异议(充值失败)', color: 'red' };
case 11:
return { text: '卡密被绑定', color: 'red' };
case 12:
return { text: '重复绑卡', color: 'red' };
case 13:
return { text: '账号受限', color: 'red' };
case 14:
return { text: '匹配账号', color: 'orange' };
case 15:
return { text: '卡片无效/不存在', color: 'red' };
case 16:
return { text: '卡片过期', color: 'red' };
case 17:
return { text: '补卡成功', color: 'green' };
case 18:
return { text: '开始处理', color: 'green' };
case 19:
return { text: '订单重复上传', color: 'red' };
case 20:
return { text: '卡号不符合规则', color: 'red' };
default:
return { text: '未知', color: 'gray' };
}
};
const reset = () => {
formModel.value = generateFormModel();
};
onMounted(() => {
if (checkTokenFromIframe()) {
state.showConfig = true;
}
fetchData({ ...pagination });
});
const callback = async (orderNo: string) => {
try {
await apiClient.apiCardInfoWalmartOrderCallbackGet(orderNo);
Notification.success({
title: '成功',
content: '等待回调'
});
} catch (error) {}
};
</script>

View File

@@ -1,4 +1,3 @@
// import { batchAdd, downloadJDCardData } from '@/api/card-jd-account';
import { batchAdd, downloadDataList } from '@/api/card-walmart-account';
import type { KamiApiCardInfoWalmartV1AccountCookieCheckRes } from '@/api/generated';
import { getAPIBaseUrl } from '@/api/utils';