feat: 新增报告结果查看并完善产品模块配置
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled

This commit is contained in:
liangzai 2025-06-05 15:16:15 +08:00
parent d3609c21c0
commit db606f10b3
10 changed files with 1341 additions and 89 deletions

View File

@ -1,59 +1 @@
import type { Recordable } from '@vben/types'; export * from './order';
import { requestClient } from '#/api/request';
export namespace OrderApi {
export interface Order {
id: number;
order_no: string;
platform_order_id: string;
product_name: string;
payment_platform: 'alipay' | 'appleiap' | 'wechat';
payment_scene: 'app' | 'h5' | 'mini_program' | 'public_account';
amount: number;
status: 'closed' | 'failed' | 'paid' | 'pending' | 'refunded';
create_time: string;
pay_time: null | string;
refund_time: null | string;
is_promotion: 0 | 1;
}
export interface OrderList {
total: number;
items: Order[];
}
export interface RefundOrderRequest {
refund_amount: number;
refund_reason: string;
}
export interface RefundOrderResponse {
status: string;
refund_no: string;
amount: number;
}
}
/**
*
*/
async function getOrderList(params: Recordable<any>) {
return requestClient.get<OrderApi.OrderList>('/order/list', {
params,
});
}
/**
* 退
* @param id ID
* @param data 退
*/
async function refundOrder(id: number, data: OrderApi.RefundOrderRequest) {
return requestClient.post<OrderApi.RefundOrderResponse>(
`/order/refund/${id}`,
data,
);
}
export { getOrderList, refundOrder };

View File

@ -0,0 +1,60 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace OrderApi {
export interface Order {
id: number;
order_no: string;
platform_order_id: string;
product_name: string;
payment_platform: 'alipay' | 'appleiap' | 'wechat';
payment_scene: 'app' | 'h5' | 'mini_program' | 'public_account';
amount: number;
status: 'closed' | 'failed' | 'paid' | 'pending' | 'refunded';
query_state: 'failed' | 'pending' | 'processing' | 'success';
create_time: string;
pay_time: null | string;
refund_time: null | string;
is_promotion: 0 | 1;
}
export interface OrderList {
total: number;
items: Order[];
}
export interface RefundOrderRequest {
refund_amount: number;
refund_reason: string;
}
export interface RefundOrderResponse {
status: string;
refund_no: string;
amount: number;
}
}
/**
*
*/
async function getOrderList(params: Recordable<any>) {
return requestClient.get<OrderApi.OrderList>('/order/list', {
params,
});
}
/**
* 退
* @param id ID
* @param data 退
*/
async function refundOrder(id: number, data: OrderApi.RefundOrderRequest) {
return requestClient.post<OrderApi.RefundOrderResponse>(
`/order/refund/${id}`,
data,
);
}
export { getOrderList, refundOrder };

View File

@ -0,0 +1,50 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace OrderQueryApi {
export interface QueryItem {
feature: Recordable<any>;
data: Recordable<any>;
}
export interface QueryDetail {
id: number;
order_id: number;
user_id: number;
product_name: string;
query_params: Recordable<any>;
query_data: QueryItem[];
create_time: string;
update_time: string;
query_state: string;
}
export interface GetQueryDetailRequest {
order_id: number;
}
export interface GetQueryDetailResponse {
id: number;
order_id: number;
user_id: number;
product_name: string;
query_params: Recordable<any>;
query_data: QueryItem[];
create_time: string;
update_time: string;
query_state: string;
}
}
/**
*
* @param orderId ID
*/
async function getOrderQueryDetail(orderId: number) {
return requestClient.get<OrderQueryApi.GetQueryDetailResponse>(
`/query/detail/${orderId}`,
);
}
export { getOrderQueryDetail };

View File

@ -77,6 +77,20 @@ export function useColumns<T = OrderApi.Order>(
title: '支付状态', title: '支付状态',
width: 120, width: 120,
}, },
{
cellRender: {
name: 'CellTag',
options: [
{ value: 'pending', color: 'warning', label: '查询中' },
{ value: 'success', color: 'success', label: '查询成功' },
{ value: 'failed', color: 'error', label: '查询失败' },
{ value: 'processing', color: 'warning', label: '查询中' },
],
},
field: 'query_state',
title: '查询状态',
width: 120,
},
{ {
field: 'create_time', field: 'create_time',
title: '创建时间', title: '创建时间',
@ -121,12 +135,19 @@ export function useColumns<T = OrderApi.Order>(
return row.status !== 'paid'; return row.status !== 'paid';
}, },
}, },
{
code: 'query',
text: '查询结果',
disabled: (row: OrderApi.Order) => {
return row.query_state !== 'success';
},
},
], ],
}, },
field: 'operation', field: 'operation',
fixed: 'right', fixed: 'right',
title: '操作', title: '操作',
width: 100, width: 180,
}, },
]; ];
} }

View File

@ -5,6 +5,8 @@ import type {
} from '#/adapter/vxe-table'; } from '#/adapter/vxe-table';
import type { OrderApi } from '#/api/order'; import type { OrderApi } from '#/api/order';
import { useRouter } from 'vue-router';
import { Page, useVbenDrawer } from '@vben/common-ui'; import { Page, useVbenDrawer } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
@ -56,8 +58,19 @@ const [RefundDrawer, refundDrawerApi] = useVbenDrawer({
destroyOnClose: true, destroyOnClose: true,
}); });
const router = useRouter();
function onActionClick(e: OnActionClickParams<OrderApi.Order>) { function onActionClick(e: OnActionClickParams<OrderApi.Order>) {
switch (e.code) { switch (e.code) {
case 'query': {
router.push({
name: 'OrderQueryDetail',
params: {
id: e.row.id,
},
});
break;
}
case 'refund': { case 'refund': {
onRefund(e.row); onRefund(e.row);
break; break;

View File

@ -0,0 +1,253 @@
<script lang="ts" setup>
import type { JsonViewerAction } from '@vben/common-ui';
import type { OrderQueryApi } from '#/api/order/query';
import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { JsonViewer, Page } from '@vben/common-ui';
import { MdiArrowLeft } from '@vben/icons';
import {
Button,
Card,
Collapse,
Descriptions,
message,
Tag,
} from 'ant-design-vue';
import { getOrderQueryDetail } from '#/api/order/query';
const route = useRoute();
const router = useRouter();
const orderId = Number(route.params.id);
const loading = ref(false);
const queryDetail = ref<OrderQueryApi.QueryDetail>();
//
const queryStateConfig = [
{ value: 'pending', color: 'warning', label: '查询中' },
{ value: 'success', color: 'success', label: '查询成功' },
{ value: 'failed', color: 'error', label: '查询失败' },
{ value: 'processing', color: 'warning', label: '查询中' },
] as const;
//
function getQueryStateConfig(state: string) {
return (
queryStateConfig.find((item) => item.value === state) || {
color: 'default',
label: state,
}
);
}
//
const fieldNameMap: Record<string, string> = {
//
name: '姓名',
id_card: '身份证号',
mobile: '手机号',
code: '验证码',
//
ent_name: '企业名称',
ent_code: '统一社会信用代码',
//
name_man: '男方姓名',
id_card_man: '男方身份证号',
name_woman: '女方姓名',
id_card_woman: '女方身份证号',
//
car_type: '车辆类型',
car_license: '车牌号',
vin_code: '车架号',
car_driving_permit: '行驶证号',
//
bank_card: '银行卡号',
//
certificate_number: '证书编号',
//
start_date: '开始日期',
};
//
function getFieldDisplayName(key: string): string {
return fieldNameMap[key] || key;
}
//
function handleBack() {
router.push('/order');
}
//
async function fetchQueryDetail() {
if (!orderId) return;
loading.value = true;
try {
const res = await getOrderQueryDetail(orderId);
queryDetail.value = res;
} catch {
message.error('获取查询详情失败');
} finally {
loading.value = false;
}
}
function handleCopied(_event: JsonViewerAction) {
message.success('已复制JSON');
}
onMounted(() => {
fetchQueryDetail();
});
</script>
<template>
<Page>
<div class="p-4">
<div class="mb-4 flex items-center">
<Button @click="handleBack">
<template #icon><MdiArrowLeft /></template>
返回订单管理
</Button>
</div>
<Card :loading="loading" class="mb-4">
<template #title>
<div class="flex items-center justify-between">
<span class="text-lg font-medium">订单查询详情</span>
<div class="flex items-center gap-2">
<span class="text-gray-500">查询状态:</span>
<Tag
v-if="queryDetail"
:color="getQueryStateConfig(queryDetail.query_state).color"
>
{{ getQueryStateConfig(queryDetail.query_state).label }}
</Tag>
</div>
</div>
</template>
<template v-if="queryDetail">
<Descriptions :column="2" bordered>
<Descriptions.Item label="订单ID">
{{ queryDetail.order_id }}
</Descriptions.Item>
<Descriptions.Item label="用户ID">
{{ queryDetail.user_id }}
</Descriptions.Item>
<Descriptions.Item label="产品名称">
{{ queryDetail.product_name }}
</Descriptions.Item>
<Descriptions.Item label="创建时间">
{{ queryDetail.create_time }}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{{ queryDetail.update_time }}
</Descriptions.Item>
</Descriptions>
</template>
</Card>
<template v-if="queryDetail">
<Card class="mb-4">
<template #title>
<span class="text-lg font-medium">查询参数</span>
</template>
<template v-if="queryDetail.query_params">
<Descriptions :column="2" bordered>
<Descriptions.Item
v-for="(value, key) in queryDetail.query_params"
:key="key"
:label="getFieldDisplayName(key)"
>
{{ value }}
</Descriptions.Item>
</Descriptions>
</template>
<div v-else class="text-gray-500">暂无查询参数</div>
</Card>
<Card>
<template #title>
<span class="text-lg font-medium">查询数据</span>
</template>
<template v-if="queryDetail.query_data?.length">
<Collapse
:default-active-key="
queryDetail.query_data.map((_, index) => index)
"
>
<Collapse.Panel
v-for="(item, index) in queryDetail.query_data"
:key="index"
>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-lg font-medium">{{
item.feature.featureName
}}</span>
<Tag color="blue">API: {{ item.data.apiID }}</Tag>
</div>
<Tag
:color="
String(item.data.success) === 'true'
? 'success'
: 'error'
"
>
{{
String(item.data.success) === 'true'
? '查询成功'
: '查询失败'
}}
</Tag>
</div>
</template>
<div class="grid gap-4">
<div v-if="item.data.data">
<div class="mb-2 font-medium">查询结果:</div>
<JsonViewer
:value="item.data.data"
copyable
:expand-depth="2"
boxed
@copied="handleCopied"
/>
</div>
<div class="text-gray-500">
查询时间: {{ item.data.timestamp }}
</div>
</div>
</Collapse.Panel>
</Collapse>
</template>
<div v-else class="py-4 text-center text-gray-500">暂无查询数据</div>
</Card>
</template>
<template v-else>
<Card>
<div class="py-8 text-center text-gray-500">
{{ loading ? '加载中...' : '暂无查询数据' }}
</div>
</Card>
</template>
</div>
</Page>
</template>
<style scoped>
:deep(.ant-collapse-header) {
padding: 12px 16px !important;
}
:deep(.ant-collapse-content-box) {
padding: 16px !important;
}
</style>

View File

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { TableColumnsType } from 'ant-design-vue'; import type { TableColumnsType } from 'ant-design-vue';
// @ts-expect-error: sortablejs
import type { SortableEvent } from 'sortablejs'; import type { SortableEvent } from 'sortablejs';
import type { import type {
@ -72,6 +73,7 @@ const [Modal, modalApi] = useVbenModal({
await updateProductFeatures(productId, { features }); await updateProductFeatures(productId, { features });
message.success('保存成功'); message.success('保存成功');
emit('success'); emit('success');
modalApi.close(); // Modal
return true; return true;
} catch { } catch {
message.error('保存失败'); message.error('保存失败');
@ -84,19 +86,15 @@ const loading = ref(false);
const tempFeatureList = ref<TempFeatureItem[]>([]); const tempFeatureList = ref<TempFeatureItem[]>([]);
// //
const [Grid, _gridApi] = useVbenVxeGrid({ const [Grid] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
submitOnChange: true, submitOnChange: true,
showCollapseButton: false,
}, },
separator: false,
gridOptions: { gridOptions: {
columns: ( columns: (useColumns(onActionClick) || []).map((col) => {
useColumns((e: OnActionClickParams<FeatureApi.FeatureItem>) => {
if (e.code === 'add' && e.row) {
handleAddFeature(e.row);
}
}) || []
).map((col) => {
if (col.field === 'operation' && col.cellRender) { if (col.field === 'operation' && col.cellRender) {
return { return {
...col, ...col,
@ -105,13 +103,23 @@ const [Grid, _gridApi] = useVbenVxeGrid({
options: [ options: [
{ {
code: 'add', code: 'add',
title: '添加', text: '添加',
show: (row: FeatureApi.FeatureItem) => { show: (row: FeatureApi.FeatureItem) => {
return !tempFeatureList.value.some( return !tempFeatureList.value.some(
(item) => item.feature_id === row.id, (item) => item.feature_id === row.id,
); );
}, },
}, },
{
code: 'added',
text: '已添加',
disabled: true,
show: (row: FeatureApi.FeatureItem) => {
return tempFeatureList.value.some(
(item) => item.feature_id === row.id,
);
},
},
], ],
}, },
}; };
@ -120,6 +128,11 @@ const [Grid, _gridApi] = useVbenVxeGrid({
}), }),
height: 500, height: 500,
keepSource: true, keepSource: true,
pagerConfig: {
pageSize: 8,
pageSizes: [8, 20, 50, 100],
pagerCount: 5,
},
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: async ({ page }, formValues) => { query: async ({ page }, formValues) => {
@ -134,13 +147,6 @@ const [Grid, _gridApi] = useVbenVxeGrid({
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
}, },
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeTableGridOptions<FeatureApi.FeatureItem>, } as VxeTableGridOptions<FeatureApi.FeatureItem>,
}); });
@ -215,10 +221,22 @@ async function loadFeatureList() {
try { try {
const res = await getProductFeatureList(productId); const res = await getProductFeatureList(productId);
// //
tempFeatureList.value = res.map((item) => ({ let tempList = res.map((item) => ({
...item, ...item,
temp_id: `existing_${item.id}`, temp_id: `existing_${item.id}`,
})); }));
// sort0sort
const allSortZero = tempList.every((item) => !item.sort || item.sort === 0);
if (allSortZero) {
tempList.forEach((item, idx) => {
item.sort = idx + 1;
});
} else {
tempList = [...tempList]
.sort((a, b) => (a.sort || 0) - (b.sort || 0))
.map((item, idx) => ({ ...item, sort: idx + 1 }));
}
tempFeatureList.value = tempList;
initSortable(); initSortable();
} finally { } finally {
loading.value = false; loading.value = false;
@ -234,7 +252,8 @@ async function initSortable() {
animation: 150, animation: 150,
handle: '.ant-table-row', handle: '.ant-table-row',
onEnd: async (evt: SortableEvent) => { onEnd: async (evt: SortableEvent) => {
const { newIndex, oldIndex } = evt; let { newIndex, oldIndex } = evt;
// undefined/null
if ( if (
typeof newIndex !== 'number' || typeof newIndex !== 'number' ||
typeof oldIndex !== 'number' || typeof oldIndex !== 'number' ||
@ -242,6 +261,10 @@ async function initSortable() {
) )
return; return;
// 1-based 0-based
newIndex = newIndex - 1;
oldIndex = oldIndex - 1;
// //
const newList = [...tempFeatureList.value]; const newList = [...tempFeatureList.value];
const [removed] = newList.splice(oldIndex, 1); const [removed] = newList.splice(oldIndex, 1);
@ -258,7 +281,15 @@ async function initSortable() {
await initializeSortable(); await initializeSortable();
} }
//
function onActionClick(e: OnActionClickParams<FeatureApi.FeatureItem>) {
switch (e.code) {
case 'add': {
handleAddFeature(e.row);
break;
}
}
}
// //
function handleAddFeature(feature: FeatureApi.FeatureItem) { function handleAddFeature(feature: FeatureApi.FeatureItem) {
// //
@ -299,20 +330,22 @@ function handleRemoveFeature(record: TempFeatureItem) {
<template> <template>
<Modal class="w-[calc(100vw-200px)]"> <Modal class="w-[calc(100vw-200px)]">
<div class="p-4"> <div class="px-2">
<div class="mb-4 text-gray-500">
提示可以通过拖拽行来调整模块顺序通过开关控制模块的启用状态和重要程度
</div>
<div class="flex gap-4"> <div class="flex gap-4">
<!-- 左侧可选模块列表 --> <!-- 左侧可选模块列表 -->
<div class="w-[600px] flex-shrink-0"> <div class="w-[600px] flex-shrink-0">
<div class="mb-2 text-base font-medium">可选模块</div> <!-- <div class="mb-2 text-base font-medium">可选模块</div>
<div class="mb-4 text-sm text-gray-500">
提示点击添加可以快速添加模块到已关联模块列表
</div> -->
<Grid /> <Grid />
</div> </div>
<!-- 右侧已关联模块列表 --> <!-- 右侧已关联模块列表 -->
<div class="flex-1"> <div class="flex-1">
<div class="mb-2 text-base font-medium">已关联模块</div> <div class="mb-2 text-base font-medium">已关联模块</div>
<div class="mb-4 text-sm text-gray-500">
提示可以通过拖拽行来调整模块顺序通过开关控制模块的启用状态和重要程度
</div>
<Table <Table
:columns="columns" :columns="columns"
:data-source="tempFeatureList" :data-source="tempFeatureList"
@ -321,6 +354,7 @@ function handleRemoveFeature(record: TempFeatureItem) {
:row-key="(record) => record.temp_id" :row-key="(record) => record.temp_id"
:scroll="{ y: 500 }" :scroll="{ y: 500 }"
class="sortable-table" class="sortable-table"
size="small"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'sort'"> <template v-if="column.dataIndex === 'sort'">

File diff suppressed because one or more lines are too long

View File

@ -56,6 +56,10 @@
"update:deps": "npx taze -r -w", "update:deps": "npx taze -r -w",
"version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile" "version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile"
}, },
"dependencies": {
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12"
},
"devDependencies": { "devDependencies": {
"@changesets/changelog-github": "catalog:", "@changesets/changelog-github": "catalog:",
"@changesets/cli": "catalog:", "@changesets/cli": "catalog:",
@ -114,9 +118,5 @@
"canvas", "canvas",
"node-gyp" "node-gyp"
] ]
},
"dependencies": {
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12"
} }
} }

View File

@ -11,3 +11,5 @@ export const MdiGithub = createIconifyIcon('mdi:github');
export const MdiGoogle = createIconifyIcon('mdi:google'); export const MdiGoogle = createIconifyIcon('mdi:google');
export const MdiQqchat = createIconifyIcon('mdi:qqchat'); export const MdiQqchat = createIconifyIcon('mdi:qqchat');
export const MdiArrowLeft = createIconifyIcon('mdi:arrow-left');