feat(财务): 新增入账管理模块
新增账单导入和账单核对功能 - 添加账单导入页面,支持微信、支付宝、银行账单文件上传 - 实现账单核对功能,包括自动核对和人工核对 - 添加多种类型订单展示组件(VIP、课程、实物商品等) - 实现订单选择和提交功能 - 添加CardList组件用于展示可选项和已选项
This commit is contained in:
17
apps/finance/src/api/posting/import.ts
Normal file
17
apps/finance/src/api/posting/import.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export const importBillsApi = {
|
||||||
|
/**
|
||||||
|
* 获取文件列表
|
||||||
|
*/
|
||||||
|
getImportFileList: (data: { month?: number; year: number }) => {
|
||||||
|
return requestClient.post('/common/import/getImportList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
*/
|
||||||
|
uploadFile: (data: any) => {
|
||||||
|
return requestClient.upload('/common/import/importData', data);
|
||||||
|
},
|
||||||
|
};
|
||||||
2
apps/finance/src/api/posting/index.ts
Normal file
2
apps/finance/src/api/posting/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './import';
|
||||||
|
export * from './reconciliate';
|
||||||
115
apps/finance/src/api/posting/reconciliate.ts
Normal file
115
apps/finance/src/api/posting/reconciliate.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export const reconciliateBillsApi = {
|
||||||
|
/**
|
||||||
|
* 获取账单列表
|
||||||
|
*/
|
||||||
|
getReconciliationList: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/getPaymentList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人工对账--订单对账
|
||||||
|
*/
|
||||||
|
manualCheckOrder: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/manualCheckoff', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人工对账--添加订单后对账
|
||||||
|
*/
|
||||||
|
manualCheckCreated: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/checkoffByAddOrder', { list: data });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动对账
|
||||||
|
*/
|
||||||
|
autoCheck: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/autoCheckoff', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单列表
|
||||||
|
*/
|
||||||
|
getOrderList: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/manualList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取吴门医述开课记录
|
||||||
|
*/
|
||||||
|
getWumenRecommendCourseBuyList: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/recommendWumenCourseBuyList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取吴门医述课程商品列表
|
||||||
|
*/
|
||||||
|
getWumenCourseBuyList: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/wumenCourseBuyList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取吴门医述充值记录列表
|
||||||
|
*/
|
||||||
|
getWumenPointBuyList: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/wumenPointBuyList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取吴门医述实物商品列表
|
||||||
|
*/
|
||||||
|
getWumenPhysicalBuyList: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/wumenPhysicalBuyList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取一路健康开课记录列表
|
||||||
|
*/
|
||||||
|
getYiluRecommendCourseBuyList: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/recommendCcourseBuyList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取一路健康课程商品列表
|
||||||
|
*/
|
||||||
|
getYiluCourseBuyList: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/courseBuyList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取一路健康充值记录列表
|
||||||
|
*/
|
||||||
|
getYiluPointBuyList: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/yljkPointBuyList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取一路健康实物商品列表
|
||||||
|
*/
|
||||||
|
getPhysicalBuyList: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/physicalBuyList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取VIP列表
|
||||||
|
*/
|
||||||
|
getVipList: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/vipList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取报名培训班列表
|
||||||
|
*/
|
||||||
|
getTrainingClassList: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/trainingClassList', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推荐用户
|
||||||
|
*/
|
||||||
|
getRecommendUser: (data: any) => {
|
||||||
|
return requestClient.post('/common/payment/recommendUser', data);
|
||||||
|
},
|
||||||
|
};
|
||||||
84
apps/finance/src/components/SelectDropdownRender/index.vue
Normal file
84
apps/finance/src/components/SelectDropdownRender/index.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineComponent, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Button, Divider, Input, Select, Space } from 'ant-design-vue';
|
||||||
|
|
||||||
|
export interface RemoteOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
/** 是否为用户自由输入生成的兜底项 */
|
||||||
|
__custom?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 选项数据 */
|
||||||
|
options?: any | { value: string }[];
|
||||||
|
/** 占位符 */
|
||||||
|
placeholder?: string;
|
||||||
|
/** v-model */
|
||||||
|
value?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:value']);
|
||||||
|
|
||||||
|
const VNodes = defineComponent({
|
||||||
|
props: {
|
||||||
|
vnodes: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return this.vnodes;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = ref<{ value: string }[]>(props.options || []);
|
||||||
|
const value = ref(props.value);
|
||||||
|
const inputRef = ref();
|
||||||
|
const inputValue = ref('');
|
||||||
|
|
||||||
|
// 监听 props.value 变化,更新本地 value
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
(newValue) => {
|
||||||
|
value.value = newValue;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听本地 value 变化,触发 update:value 事件
|
||||||
|
watch(value, (newValue) => {
|
||||||
|
emit('update:value', newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
const addItem = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newItemValue = inputValue.value;
|
||||||
|
items.value.push({
|
||||||
|
value: newItemValue,
|
||||||
|
});
|
||||||
|
// 将新添加的选项设为选中值
|
||||||
|
value.value = newItemValue;
|
||||||
|
inputValue.value = '';
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.value?.focus();
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Select v-model:value="value" placeholder="请选择或新增" :options="items">
|
||||||
|
<template #dropdownRender="{ menuNode: menu }">
|
||||||
|
<VNodes :vnodes="menu" />
|
||||||
|
<Divider style="margin: 4px 0" />
|
||||||
|
<Space style="padding: 4px 8px">
|
||||||
|
<Input ref="inputRef" v-model:value="inputValue" placeholder="在此手动输入" />
|
||||||
|
<Button type="text" @click="addItem">
|
||||||
|
<template #icon>
|
||||||
|
<!-- <PlusOutlined /> -->
|
||||||
|
</template>
|
||||||
|
新增
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
</Select>
|
||||||
|
</template>
|
||||||
197
apps/finance/src/components/card-list/api.ts
Normal file
197
apps/finance/src/components/card-list/api.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import type { CardListApi, CardListPagination, CardListProps } from './types';
|
||||||
|
|
||||||
|
import { bindMethods, isFunction, mergeWithArrayOverride, StateHandler } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Store } from '@vben-core/shared/store';
|
||||||
|
|
||||||
|
function getDefaultState(): CardListProps {
|
||||||
|
return {
|
||||||
|
class: '',
|
||||||
|
gridClass: '',
|
||||||
|
gridOptions: {
|
||||||
|
columns: [],
|
||||||
|
data: [],
|
||||||
|
showTitle: true,
|
||||||
|
gridColumns: 3,
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: true,
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total: number) => `共 ${total} 条`,
|
||||||
|
},
|
||||||
|
cardHeight: 'auto',
|
||||||
|
cardWidth: '100%',
|
||||||
|
cardGap: '10px',
|
||||||
|
loading: false,
|
||||||
|
emptyText: '暂无数据',
|
||||||
|
},
|
||||||
|
gridEvents: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CardListApiInstance<T extends Record<string, any> = any> implements CardListApi<T> {
|
||||||
|
public state: CardListProps<T> | null = null;
|
||||||
|
public store: Store<CardListProps<T>>;
|
||||||
|
|
||||||
|
private isMounted = false;
|
||||||
|
private stateHandler: StateHandler;
|
||||||
|
|
||||||
|
constructor(options: CardListProps = {}) {
|
||||||
|
const storeState = { ...options };
|
||||||
|
|
||||||
|
const defaultState = getDefaultState();
|
||||||
|
this.store = new Store<CardListProps>(mergeWithArrayOverride(storeState, defaultState), {
|
||||||
|
onUpdate: () => {
|
||||||
|
this.state = this.store.state;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.state = this.store.state;
|
||||||
|
this.stateHandler = new StateHandler();
|
||||||
|
bindMethods(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.setData([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
return (this.state?.gridOptions?.data || []).map((item) => ({ ...item })) as T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 检查组件是否已挂载 */
|
||||||
|
getIsMounted() {
|
||||||
|
return this.isMounted;
|
||||||
|
}
|
||||||
|
|
||||||
|
getState() {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加项目 */
|
||||||
|
insertAt(items: T[], index?: number) {
|
||||||
|
const data = this.getData();
|
||||||
|
const newData = [...data];
|
||||||
|
|
||||||
|
if (typeof index === 'number' && index >= 0 && index <= newData.length) {
|
||||||
|
newData.splice(index, 0, ...items);
|
||||||
|
} else {
|
||||||
|
// 需判断如果原数据已经存在,则不添加
|
||||||
|
newData.push(...items);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setData(newData);
|
||||||
|
}
|
||||||
|
|
||||||
|
mount() {
|
||||||
|
this.isMounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
// 触发数据刷新
|
||||||
|
const currentData = this.state?.gridOptions?.data || [];
|
||||||
|
this.setData([...currentData] as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(predicate: ((row: T, index: number) => boolean) | number) {
|
||||||
|
const data = this.getData();
|
||||||
|
|
||||||
|
const newData =
|
||||||
|
typeof predicate === 'number'
|
||||||
|
? data.filter((_, index) => index !== predicate)
|
||||||
|
: data.filter((row, index) => !predicate(row, index));
|
||||||
|
|
||||||
|
this.setData(newData);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
const defaultState = getDefaultState();
|
||||||
|
this.setState({
|
||||||
|
gridOptions: defaultState.gridOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(data: T[]) {
|
||||||
|
this.setState({
|
||||||
|
gridOptions: {
|
||||||
|
...this.state?.gridOptions,
|
||||||
|
data: data as any,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorItems(errorItems: any[]) {
|
||||||
|
// 遍历data,将errorItems中的id匹配到的item添加error属性
|
||||||
|
const newData = this.getData().map((item) => {
|
||||||
|
const errorData = errorItems.find((error) => error.id === item.id);
|
||||||
|
return errorData ? { ...item, error: true } : { ...item, error: false };
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
gridOptions: {
|
||||||
|
...this.state?.gridOptions,
|
||||||
|
data: newData as any,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(loading: boolean) {
|
||||||
|
this.setState({
|
||||||
|
gridOptions: {
|
||||||
|
...this.state?.gridOptions,
|
||||||
|
loading,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setPagination(pagination: any) {
|
||||||
|
// 确保分页对象符合CardListPagination类型要求
|
||||||
|
const validPagination: CardListPagination = {
|
||||||
|
current: pagination?.current || 1,
|
||||||
|
pageSize: pagination?.pageSize || 10,
|
||||||
|
total: pagination?.total,
|
||||||
|
showSizeChanger: pagination?.showSizeChanger,
|
||||||
|
showQuickJumper: pagination?.showQuickJumper,
|
||||||
|
showTotal: pagination?.showTotal,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
gridOptions: {
|
||||||
|
...this.state?.gridOptions,
|
||||||
|
pagerConfig: validPagination,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(
|
||||||
|
state: ((prev: CardListProps<T>) => Partial<CardListProps<T>>) | Partial<CardListProps<T>>,
|
||||||
|
) {
|
||||||
|
if (isFunction(state)) {
|
||||||
|
const stateFn = state as (prev: CardListProps<T>) => Partial<CardListProps<T>>;
|
||||||
|
this.store.setState((prev: any) => {
|
||||||
|
return mergeWithArrayOverride(stateFn(prev), prev);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.store.setState((prev: any) => mergeWithArrayOverride(state, prev));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount() {
|
||||||
|
this.isMounted = false;
|
||||||
|
this.stateHandler.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateItem(predicate: ((row: T, index: number) => boolean) | number, newItem: Partial<T>) {
|
||||||
|
const data = this.getData();
|
||||||
|
|
||||||
|
const newData = data.map((row, index) => {
|
||||||
|
const matched = typeof predicate === 'number' ? index === predicate : predicate(row, index);
|
||||||
|
|
||||||
|
return matched ? { ...row, ...newItem } : row;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setData(newData);
|
||||||
|
}
|
||||||
|
}
|
||||||
393
apps/finance/src/components/card-list/card-list.vue
Normal file
393
apps/finance/src/components/card-list/card-list.vue
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CardListProps, ExtendedCardListApi } from './types';
|
||||||
|
|
||||||
|
import { computed, h, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { usePriorityValues } from '@vben/hooks';
|
||||||
|
import { cn } from '@vben/utils';
|
||||||
|
|
||||||
|
import { VbenLoading } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
import { Card, DatePicker, Empty, Input, Select } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import SelectDropdownRender from '#/components/SelectDropdownRender/index.vue';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {});
|
||||||
|
|
||||||
|
// 创建一个全局缓存对象
|
||||||
|
const selectOptionsCache: Record<string, any[]> = {};
|
||||||
|
|
||||||
|
interface Props extends CardListProps {
|
||||||
|
api: ExtendedCardListApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listData = computed(() => {
|
||||||
|
return gridOptions.value?.data || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = props.api?.useStore?.();
|
||||||
|
|
||||||
|
const { class: className, gridClass, gridOptions, gridEvents } = usePriorityValues(props, state);
|
||||||
|
|
||||||
|
// 计算标题字段
|
||||||
|
const titleFieldName = computed(() => {
|
||||||
|
const titleField = gridOptions.value?.titleField;
|
||||||
|
if (titleField) return titleField;
|
||||||
|
const columns = gridOptions.value?.columns || [];
|
||||||
|
if (columns.length > 0) return columns[0]?.field || '';
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 卡片网格样式
|
||||||
|
const gridStyle = computed(() => {
|
||||||
|
const gridHeight = gridOptions.value?.gridHeight || 'auto';
|
||||||
|
const gridColumns = gridOptions.value?.gridColumns || 3;
|
||||||
|
const cardGap = gridOptions.value?.cardGap || '10px';
|
||||||
|
|
||||||
|
return {
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
|
||||||
|
gap: cardGap,
|
||||||
|
height: gridHeight,
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: '5px',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 卡片样式
|
||||||
|
const cardStyle = (item: any) => {
|
||||||
|
const cardHeight = gridOptions.value?.cardHeight;
|
||||||
|
const cardWidth = gridOptions.value?.cardWidth || '100%';
|
||||||
|
|
||||||
|
const style: any = {
|
||||||
|
width: cardWidth,
|
||||||
|
minWidth: 0, // 添加最小宽度为0,防止内容撑开
|
||||||
|
overflow: 'hidden', // 添加溢出隐藏
|
||||||
|
borderColor: item.error ? 'red' : 'inherit',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cardHeight && cardHeight !== 'auto') {
|
||||||
|
style.height = cardHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
|
// const errorCardStyle = (item: any) => {
|
||||||
|
// const errorData = gridOptions.value?.errorData || [];
|
||||||
|
// return errorData.some((errorItem) => errorItem.id === item.id)
|
||||||
|
// ? {
|
||||||
|
// ...cardStyle.value,
|
||||||
|
// borderColor: 'red',
|
||||||
|
// }
|
||||||
|
// : false;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// 格式化字段值
|
||||||
|
const formatFieldValue = (column: any, row: any) => {
|
||||||
|
if (!column?.field) return '';
|
||||||
|
const value = row[column.field];
|
||||||
|
if (column?.formatter) {
|
||||||
|
return column.formatter(value, row);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染编辑组件
|
||||||
|
const renderEditComponent = (column: any, row: any) => {
|
||||||
|
const fieldName = column.field;
|
||||||
|
const value = row[fieldName];
|
||||||
|
const editRender = column.editRender;
|
||||||
|
const editProps = editRender?.props || {};
|
||||||
|
|
||||||
|
// 创建更新函数,避免直接修改只读对象
|
||||||
|
const updateValue = (val: any) => {
|
||||||
|
// 获取当前数据数组
|
||||||
|
const currentData = gridOptions.value?.data || [];
|
||||||
|
// 找到要修改的项的索引
|
||||||
|
const index = currentData.findIndex((item: any) => item.id === row.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
// 创建新的数据数组,避免直接修改原数组
|
||||||
|
const newData = [...currentData];
|
||||||
|
// 创建新的对象,避免直接修改原对象
|
||||||
|
newData[index] = { ...newData[index], [fieldName]: val };
|
||||||
|
// 更新数据
|
||||||
|
props.api.setData(newData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (editRender.name) {
|
||||||
|
case 'date-picker':
|
||||||
|
case 'DatePicker': {
|
||||||
|
return h(DatePicker, {
|
||||||
|
value,
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
...editProps,
|
||||||
|
'onUpdate:value': updateValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'Input':
|
||||||
|
case 'input': {
|
||||||
|
return h(Input, {
|
||||||
|
value,
|
||||||
|
...editProps,
|
||||||
|
'onUpdate:value': updateValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'input-number':
|
||||||
|
case 'InputNumber': {
|
||||||
|
return h(Input, {
|
||||||
|
value,
|
||||||
|
type: 'number',
|
||||||
|
...editProps,
|
||||||
|
'onUpdate:value': updateValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'Select':
|
||||||
|
case 'select': {
|
||||||
|
return h(Select, {
|
||||||
|
value,
|
||||||
|
...editProps,
|
||||||
|
'onUpdate:value': updateValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'SelectDropdownRender':
|
||||||
|
case 'selectDropdownRender': {
|
||||||
|
// 创建一个响应式的选项数组
|
||||||
|
const options = ref<any[]>([]);
|
||||||
|
|
||||||
|
// 创建一个唯一的缓存键,基于 row 的关键属性
|
||||||
|
const cacheKey = `${row.paymentId}_${row.come}_${row.orderType}_${row.courseId}`;
|
||||||
|
|
||||||
|
// 检查缓存中是否已有数据
|
||||||
|
if (selectOptionsCache[cacheKey]) {
|
||||||
|
options.value = selectOptionsCache[cacheKey];
|
||||||
|
} else if (editProps?.fetchOptions) {
|
||||||
|
// 如果缓存中没有,则获取数据并存储到缓存
|
||||||
|
editProps.fetchOptions(row).then((res: any) => {
|
||||||
|
options.value = res;
|
||||||
|
// 存储到缓存
|
||||||
|
selectOptionsCache[cacheKey] = res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return h(SelectDropdownRender, {
|
||||||
|
value,
|
||||||
|
...editProps,
|
||||||
|
options,
|
||||||
|
'onUpdate:value': updateValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return formatFieldValue(column, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理卡片点击
|
||||||
|
const handleCardClick = (row: any) => {
|
||||||
|
gridEvents.value?.cardClick?.(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
async function init() {
|
||||||
|
await nextTick();
|
||||||
|
if (props.api?.mount) {
|
||||||
|
props.api.mount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (props.api?.unmount) {
|
||||||
|
props.api.unmount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="cn('h-full overflow-auto rounded-md', className)">
|
||||||
|
<div :class="cn('rounded-md bg-card', gridClass)">
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<VbenLoading v-if="gridOptions?.loading" :spinning="true" />
|
||||||
|
|
||||||
|
<!-- 卡片列表 -->
|
||||||
|
<div v-else-if="listData.length > 0" :style="gridStyle">
|
||||||
|
<Card
|
||||||
|
v-for="(item, index) in listData"
|
||||||
|
:key="item[titleFieldName]"
|
||||||
|
:class="gridOptions?.cardClass"
|
||||||
|
:style="cardStyle(item)"
|
||||||
|
@click="handleCardClick(item)"
|
||||||
|
>
|
||||||
|
<!-- 自定义标题 -->
|
||||||
|
<template #title v-if="gridOptions?.showTitle">
|
||||||
|
<div class="custom-card-title" :title="item[titleFieldName]">
|
||||||
|
{{ item[titleFieldName] }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 卡片右上角额外内容 -->
|
||||||
|
<template #extra v-if="$slots['card-extra']">
|
||||||
|
<slot name="card-extra" :row="item" :index="index"></slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<div
|
||||||
|
v-for="column in gridOptions?.columns"
|
||||||
|
:key="column?.field || ''"
|
||||||
|
class="card-field"
|
||||||
|
v-show="
|
||||||
|
!column?.show ||
|
||||||
|
(typeof column.show === 'function' ? column.show(item) : column.show)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- 自定义插槽 -->
|
||||||
|
<div v-if="column?.slots?.default" class="field-item">
|
||||||
|
<span class="field-title">{{ column?.title }}:</span>
|
||||||
|
<span class="field-value field-edit">
|
||||||
|
<slot
|
||||||
|
:name="column.slots.default"
|
||||||
|
:row="item"
|
||||||
|
:field="column?.field || ''"
|
||||||
|
:value="item[column?.field || '']"
|
||||||
|
:index="index"
|
||||||
|
></slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 编辑渲染组件 -->
|
||||||
|
<div v-else-if="column?.editRender" class="field-item">
|
||||||
|
<span class="field-title">{{ column?.title }}:</span>
|
||||||
|
<span class="field-value field-edit">
|
||||||
|
<component :is="renderEditComponent(column, item)" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 默认显示 -->
|
||||||
|
<div v-else class="field-item">
|
||||||
|
<span class="field-title">{{ column?.title }}:</span>
|
||||||
|
<span class="field-value">{{ formatFieldValue(column, item) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片底部操作区域 -->
|
||||||
|
<template #actions v-if="$slots['card-actions']">
|
||||||
|
<slot name="card-actions" :row="item"></slot>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else class="flex flex-col items-center justify-center py-10">
|
||||||
|
<Empty :description="gridOptions?.emptyText || '暂无数据'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card-content {
|
||||||
|
.card-field {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.field-title {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-value {
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-all;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
|
||||||
|
&.field-edit {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:deep(.ant-input),
|
||||||
|
:deep(.ant-picker),
|
||||||
|
:deep(.ant-select) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%; // 确保卡片不超出容器宽度
|
||||||
|
min-width: 0; // 防止内容撑开
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 12px !important;
|
||||||
|
min-width: 0; // 防止内容撑开
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-width: 0; // 防止内容撑开
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-head {
|
||||||
|
min-height: auto;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
min-width: 0; // 防止内容撑开
|
||||||
|
height: auto; // 允许头部高度自适应
|
||||||
|
display: flex; // 使用flex布局
|
||||||
|
|
||||||
|
.ant-card-head-wrapper {
|
||||||
|
align-items: flex-start; // 顶部对齐
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-head-title {
|
||||||
|
padding: 8px 0;
|
||||||
|
width: 100%;
|
||||||
|
height: auto; // 允许标题高度自适应
|
||||||
|
white-space: normal; // 允许换行
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-extra {
|
||||||
|
padding: 8px 0; // 给extra区域添加与标题相同的内边距
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-card-title {
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
white-space: normal; // 允许换行
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
11
apps/finance/src/components/card-list/index.ts
Normal file
11
apps/finance/src/components/card-list/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export { CardListApiInstance } from './api';
|
||||||
|
export { default as CardList } from './card-list.vue';
|
||||||
|
export type {
|
||||||
|
CardListApi,
|
||||||
|
CardListColumn,
|
||||||
|
CardListOptions,
|
||||||
|
CardListPagination,
|
||||||
|
CardListProps,
|
||||||
|
ExtendedCardListApi,
|
||||||
|
} from './types';
|
||||||
|
export { useCardList } from './use-card-list';
|
||||||
134
apps/finance/src/components/card-list/types.ts
Normal file
134
apps/finance/src/components/card-list/types.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { ClassType, DeepPartial } from '@vben/types';
|
||||||
|
|
||||||
|
export interface CardListColumn<T = any> {
|
||||||
|
/** 字段名 */
|
||||||
|
field: string;
|
||||||
|
/** 显示标题 */
|
||||||
|
title: string;
|
||||||
|
/** 格式化函数 */
|
||||||
|
formatter?: (value: any, row: T) => string;
|
||||||
|
/** 插槽配置 */
|
||||||
|
slots?: {
|
||||||
|
default?: string;
|
||||||
|
};
|
||||||
|
/** 显示条件 */
|
||||||
|
show?: ((row: T) => boolean) | boolean;
|
||||||
|
/** 编辑渲染组件 */
|
||||||
|
editRender?: { name: string; props?: Record<string, any> };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardListPagination {
|
||||||
|
/** 是否启用分页 */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** 当前页码 */
|
||||||
|
current?: number;
|
||||||
|
/** 每页条数 */
|
||||||
|
pageSize?: number;
|
||||||
|
/** 总条数 */
|
||||||
|
total?: number;
|
||||||
|
/** 显示分页大小选择器 */
|
||||||
|
showSizeChanger?: boolean;
|
||||||
|
/** 显示快速跳转 */
|
||||||
|
showQuickJumper?: boolean;
|
||||||
|
/** 显示总条数 */
|
||||||
|
showTotal?: ((total: number, range: [number, number]) => string) | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardListOptions<T = any> {
|
||||||
|
/** 列配置 */
|
||||||
|
columns?: CardListColumn<T>[];
|
||||||
|
/** 数据源 */
|
||||||
|
data?: T[];
|
||||||
|
/** 是否显示卡片标题 */
|
||||||
|
showTitle?: boolean;
|
||||||
|
/** 标题字段名,如果不指定则使用第一个字段 */
|
||||||
|
titleField?: string;
|
||||||
|
/** 卡片类名 */
|
||||||
|
cardClass?: string;
|
||||||
|
/** 网格列数 */
|
||||||
|
gridColumns?: number;
|
||||||
|
/** 网格高度 */
|
||||||
|
gridHeight?: string;
|
||||||
|
/** 分页配置 */
|
||||||
|
pagerConfig?: CardListPagination;
|
||||||
|
/** 卡片高度 */
|
||||||
|
cardHeight?: string;
|
||||||
|
/** 卡片宽度 */
|
||||||
|
cardWidth?: string;
|
||||||
|
/** 卡片间距 */
|
||||||
|
cardGap?: string;
|
||||||
|
/** 加载状态 */
|
||||||
|
loading?: boolean;
|
||||||
|
/** 空状态文本 */
|
||||||
|
emptyText?: string;
|
||||||
|
/** 错误数据 */
|
||||||
|
errorData?: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardListProps<T = any> {
|
||||||
|
/** 标题 */
|
||||||
|
title?: string;
|
||||||
|
/** 标题帮助 */
|
||||||
|
titleHelp?: string;
|
||||||
|
/** 组件class */
|
||||||
|
class?: ClassType;
|
||||||
|
/** 网格class */
|
||||||
|
gridClass?: ClassType;
|
||||||
|
/** 网格配置 */
|
||||||
|
gridOptions?: DeepPartial<CardListOptions<T>>;
|
||||||
|
/** 网格事件 */
|
||||||
|
gridEvents?: {
|
||||||
|
/** 卡片点击 */
|
||||||
|
cardClick?: (row: T) => void;
|
||||||
|
/** 页码变化 */
|
||||||
|
pageChange?: (page: number, pageSize: number) => void;
|
||||||
|
/** 每页条数变化 */
|
||||||
|
pageSizeChange?: (pageSize: number) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CardListApi<T = any> = {
|
||||||
|
/** 清空数据 */
|
||||||
|
clear: () => void;
|
||||||
|
/** 获取数据 */
|
||||||
|
getData: () => T[];
|
||||||
|
/** 获取状态 */
|
||||||
|
getState: () => CardListProps<T> | null;
|
||||||
|
/** 添加项目 */
|
||||||
|
insertAt: (items: T[], index?: number) => void;
|
||||||
|
/** 挂载组件 */
|
||||||
|
mount: () => void;
|
||||||
|
/** 刷新数据 */
|
||||||
|
refresh: () => void;
|
||||||
|
/** 删除项目 */
|
||||||
|
remove: (predicate: ((row: T, index: number) => boolean) | number) => void;
|
||||||
|
/** 重置 */
|
||||||
|
reset: () => void;
|
||||||
|
/** 设置数据 */
|
||||||
|
setData: (data: T[]) => void;
|
||||||
|
/** 设置错误数据 */
|
||||||
|
setErrorItems: (errorItems: { id: number | string }[]) => void;
|
||||||
|
/** 设置加载状态 */
|
||||||
|
setLoading: (loading: boolean) => void;
|
||||||
|
/** 设置分页 */
|
||||||
|
setPagination: (pagination: CardListPagination) => void;
|
||||||
|
/** 设置状态 */
|
||||||
|
setState: (state: Partial<CardListProps<T>>) => void;
|
||||||
|
/** 卸载组件 */
|
||||||
|
unmount: () => void;
|
||||||
|
/** 更新项目 */
|
||||||
|
updateItem: (
|
||||||
|
predicate: ((row: T, index: number) => boolean) | number,
|
||||||
|
newItem: Partial<T>,
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtendedCardListApi<T = any> = CardListApi<T> & {
|
||||||
|
useStore: <R = CardListProps<T>>(selector?: (state: CardListProps<T>) => R) => Readonly<Ref<R>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseVbenCardList = <T extends Record<string, any> = any>(
|
||||||
|
options: CardListProps<T>,
|
||||||
|
) => readonly [any, ExtendedCardListApi<T>];
|
||||||
41
apps/finance/src/components/card-list/use-card-list.ts
Normal file
41
apps/finance/src/components/card-list/use-card-list.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { SlotsType } from 'vue';
|
||||||
|
|
||||||
|
import type { CardListProps, ExtendedCardListApi } from './types';
|
||||||
|
|
||||||
|
import { defineComponent, h, onBeforeUnmount } from 'vue';
|
||||||
|
|
||||||
|
import { useStore } from '@vben-core/shared/store';
|
||||||
|
|
||||||
|
import { CardListApiInstance } from './api';
|
||||||
|
import CardList from './card-list.vue';
|
||||||
|
|
||||||
|
export function useCardList<T extends Record<string, any> = any>(options: CardListProps<T>) {
|
||||||
|
const api = new CardListApiInstance<T>(options);
|
||||||
|
const extendedApi = api as any as ExtendedCardListApi<T>;
|
||||||
|
|
||||||
|
extendedApi.useStore = (selector) => {
|
||||||
|
return useStore(api.store, selector);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CardListComponent = defineComponent(
|
||||||
|
(props: CardListProps<T>, { attrs, slots }) => {
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
api.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
api.setState({ ...props, ...attrs });
|
||||||
|
return () => h(CardList, { ...props, ...attrs, api: extendedApi }, slots);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CardList',
|
||||||
|
inheritAttrs: false,
|
||||||
|
slots: Object as SlotsType<{
|
||||||
|
[key: string]: { field: string; index: number; row: T; value: any };
|
||||||
|
}>,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return [CardListComponent, extendedApi] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type useCardList = typeof useCardList;
|
||||||
36
apps/finance/src/router/routes/modules/posting.ts
Normal file
36
apps/finance/src/router/routes/modules/posting.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
icon: 'ic:baseline-view-in-ar',
|
||||||
|
keepAlive: true,
|
||||||
|
order: 1000,
|
||||||
|
title: '入账管理',
|
||||||
|
},
|
||||||
|
name: 'Posting',
|
||||||
|
path: '/posting',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
title: '账单导入',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
name: 'ImportBills',
|
||||||
|
path: '/posting/import-bills',
|
||||||
|
component: () => import('#/views/posting/import/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
title: '账单核对',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
name: 'ReconciliateBills',
|
||||||
|
path: '/posting/reconciliate-bills',
|
||||||
|
component: () => import('#/views/posting/reconciliate/index.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
100
apps/finance/src/views/posting/import/components/FileBox.vue
Normal file
100
apps/finance/src/views/posting/import/components/FileBox.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Modal, notification, Tag, Upload } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { importBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
row: any;
|
||||||
|
type: string;
|
||||||
|
field: string;
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {});
|
||||||
|
|
||||||
|
// 定义自定义事件
|
||||||
|
const emit = defineEmits<{
|
||||||
|
uploadSuccess: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const uploadRef = ref<any>(null);
|
||||||
|
|
||||||
|
function downloadFile(url: string) {
|
||||||
|
window.open(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickUpload() {
|
||||||
|
if (props.row[props.field]) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: '重传会清空已上传的数据,确认重传吗?',
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk() {
|
||||||
|
openFileDialog();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
openFileDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFileDialog() {
|
||||||
|
const input = uploadRef.value?.$el?.querySelector('input[type="file"]');
|
||||||
|
input && input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeUpload() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFile(options: any) {
|
||||||
|
const { file } = options;
|
||||||
|
|
||||||
|
// 使用 requestClient.upload 方法,它会自动处理 FormData
|
||||||
|
await importBillsApi.uploadFile({
|
||||||
|
file,
|
||||||
|
year: props.row.year,
|
||||||
|
month: props.row.month,
|
||||||
|
type: props.type,
|
||||||
|
});
|
||||||
|
notification.success({
|
||||||
|
message: '上传成功',
|
||||||
|
});
|
||||||
|
|
||||||
|
// message.success(`上传成功,大概需要10分钟解析核对数据,请耐心等待稍后查询。`);
|
||||||
|
// 通知父组件刷新列表
|
||||||
|
emit('uploadSuccess');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
<Tag v-if="props.row[props.field]" color="green">已上传</Tag>
|
||||||
|
<Tag v-else color="orange">未上传</Tag>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Tag v-if="props.row[props.field]" class="btn" @click="downloadFile(props.row[props.field])">
|
||||||
|
下载文件
|
||||||
|
</Tag>
|
||||||
|
|
||||||
|
<Upload
|
||||||
|
ref="uploadRef"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:custom-request="uploadFile"
|
||||||
|
:show-upload-list="false"
|
||||||
|
:open-file-dialog-on-click="false"
|
||||||
|
>
|
||||||
|
<Tag class="btn" @click="handleClickUpload">
|
||||||
|
{{ props.row[props.field] ? '点击重传' : '点击上传' }}
|
||||||
|
</Tag>
|
||||||
|
</Upload>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.btn {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
141
apps/finance/src/views/posting/import/index.vue
Normal file
141
apps/finance/src/views/posting/import/index.vue
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { importBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
import FileBox from './components/FileBox.vue';
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
year: string;
|
||||||
|
month: string;
|
||||||
|
id: string;
|
||||||
|
wechatFile: string;
|
||||||
|
alipayFile: string;
|
||||||
|
bankFile: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formOptions: VbenFormProps = {
|
||||||
|
// 默认展开
|
||||||
|
collapsed: false,
|
||||||
|
wrapperClass: 'grid-cols-6',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: false,
|
||||||
|
picker: 'year',
|
||||||
|
valueFormat: 'YYYY',
|
||||||
|
},
|
||||||
|
defaultValue: `${dayjs().year()}`,
|
||||||
|
fieldName: 'year',
|
||||||
|
label: '年份',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: Array.from({ length: 12 }, (_, i) => ({
|
||||||
|
label: `${i + 1}月`,
|
||||||
|
value: `${i + 1}`,
|
||||||
|
})),
|
||||||
|
placeholder: '请选择',
|
||||||
|
},
|
||||||
|
fieldName: 'month',
|
||||||
|
label: '月份',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 控制表单是否显示折叠按钮
|
||||||
|
showCollapseButton: false,
|
||||||
|
// 是否在字段值改变时提交表单
|
||||||
|
submitOnChange: true,
|
||||||
|
// 按下回车时是否提交表单
|
||||||
|
submitOnEnter: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridOptions: VxeTableGridOptions<RowType> = {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
title: '日期',
|
||||||
|
slots: {
|
||||||
|
default: 'date',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'wechatFile',
|
||||||
|
title: '微信',
|
||||||
|
slots: {
|
||||||
|
default: 'wechat-file',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'alipayFile',
|
||||||
|
title: '支付宝',
|
||||||
|
slots: {
|
||||||
|
default: 'alipay-file',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'bankFile',
|
||||||
|
title: '银行',
|
||||||
|
slots: {
|
||||||
|
default: 'bank-file',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exportConfig: {},
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
rowConfig: {
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
response: {
|
||||||
|
result: 'importList',
|
||||||
|
list: 'importList',
|
||||||
|
},
|
||||||
|
ajax: {
|
||||||
|
query: async (_, formValues) => {
|
||||||
|
return await importBillsApi.getImportFileList({
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions,
|
||||||
|
gridOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理上传成功事件,刷新列表
|
||||||
|
function handleUploadSuccess() {
|
||||||
|
gridApi?.query();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<Grid>
|
||||||
|
<template #date="{ row }"> {{ row.year }}年{{ row.month }}月 </template>
|
||||||
|
<template #wechat-file="{ row }">
|
||||||
|
<FileBox :row="row" field="wechatFile" type="0" @upload-success="handleUploadSuccess" />
|
||||||
|
</template>
|
||||||
|
<template #alipay-file="{ row }">
|
||||||
|
<FileBox :row="row" field="alipayFile" type="1" @upload-success="handleUploadSuccess" />
|
||||||
|
</template>
|
||||||
|
<template #bank-file="{ row }">
|
||||||
|
<FileBox :row="row" field="bankFile" type="2" @upload-success="handleUploadSuccess" />
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import { Button } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
paymentId: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['completeCheck']);
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
id: string;
|
||||||
|
source: string;
|
||||||
|
orderSn: string;
|
||||||
|
tel: string;
|
||||||
|
fee: string;
|
||||||
|
type: string;
|
||||||
|
orderTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formOptions: VbenFormProps = {
|
||||||
|
// 默认展开
|
||||||
|
collapsed: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
optionType: 'button',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '金额',
|
||||||
|
value: '0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: '请选择',
|
||||||
|
},
|
||||||
|
defaultValue: '0',
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '匹配方式',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 控制表单是否显示折叠按钮
|
||||||
|
showCollapseButton: false,
|
||||||
|
// 是否在字段值改变时提交表单
|
||||||
|
submitOnChange: true,
|
||||||
|
// 按下回车时是否提交表单
|
||||||
|
submitOnEnter: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatSource = (type: number) => {
|
||||||
|
switch (type) {
|
||||||
|
case 0: {
|
||||||
|
return '一路健康';
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
return '吴门医述';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatType = (type: number) => {
|
||||||
|
switch (type) {
|
||||||
|
case 0: {
|
||||||
|
return '充值';
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
return 'vip';
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
return '课';
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
return '实物';
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
return '培训班';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridOptions: VxeTableGridOptions<RowType> = {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'source',
|
||||||
|
title: '来源',
|
||||||
|
formatter: ({ cellValue }) => {
|
||||||
|
return formatSource(cellValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'orderSn',
|
||||||
|
title: '订单号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'tel',
|
||||||
|
title: '电话',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'fee',
|
||||||
|
title: '金额',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
title: '订单类型',
|
||||||
|
formatter: ({ cellValue }) => {
|
||||||
|
return formatType(cellValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'orderTime',
|
||||||
|
title: '订单时间',
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'action' },
|
||||||
|
title: '操作',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exportConfig: {},
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
rowConfig: {
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
response: {
|
||||||
|
result: 'records',
|
||||||
|
list: 'records',
|
||||||
|
total: 'total',
|
||||||
|
},
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await reconciliateBillsApi.getOrderList({
|
||||||
|
page: page.currentPage,
|
||||||
|
limit: page.pageSize,
|
||||||
|
paymentId: props.paymentId.toString(),
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions,
|
||||||
|
gridOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onConfirm(row: RowType) {
|
||||||
|
await reconciliateBillsApi.manualCheckOrder({
|
||||||
|
paymentId: props.paymentId,
|
||||||
|
orderId: row.id,
|
||||||
|
});
|
||||||
|
emit('completeCheck');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听paymentId变化,重新加载数据
|
||||||
|
watch(
|
||||||
|
() => props.paymentId,
|
||||||
|
(newPaymentId, oldPaymentId) => {
|
||||||
|
if (newPaymentId && newPaymentId !== oldPaymentId) {
|
||||||
|
// 重新加载数据
|
||||||
|
gridApi?.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: false },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="orders-container h-full">
|
||||||
|
<Grid>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<Button type="primary" size="small" @click="onConfirm(row)">确认对账</Button>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.orders-container {
|
||||||
|
:deep(.vxe-table) {
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CreateOrderType } from '../types';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Button } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting/reconciliate';
|
||||||
|
import { useCardList } from '#/components/card-list/index';
|
||||||
|
|
||||||
|
interface RecommendedUser {
|
||||||
|
tel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
errorItem: CreateOrderType[];
|
||||||
|
}>(),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['deletedChecked']);
|
||||||
|
|
||||||
|
const errorData = ref<any[]>([]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.errorItem,
|
||||||
|
(newVal) => {
|
||||||
|
errorData.value = newVal.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
}));
|
||||||
|
gridApi.setErrorItems(errorData.value);
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridOptions = computed(() => ({
|
||||||
|
columns: [
|
||||||
|
// { field: 'productName', title: '名称' },
|
||||||
|
{
|
||||||
|
field: 'orderType',
|
||||||
|
title: '类型',
|
||||||
|
formatter: (value: string) => {
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
'0': '充值',
|
||||||
|
'1': 'VIP',
|
||||||
|
'2': '课程',
|
||||||
|
'3': '实物',
|
||||||
|
'4': '培训班',
|
||||||
|
};
|
||||||
|
return typeMap[value] || value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'come',
|
||||||
|
title: '来源',
|
||||||
|
formatter: (value: string) => {
|
||||||
|
const comeMap: Record<string, string> = {
|
||||||
|
'0': '一路健康',
|
||||||
|
'1': '吴门医述',
|
||||||
|
'2': '手动添加',
|
||||||
|
};
|
||||||
|
return comeMap[value] || value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
editRender: {
|
||||||
|
name: 'SelectDropdownRender',
|
||||||
|
props: {
|
||||||
|
fetchOptions: (row: CreateOrderType) =>
|
||||||
|
reconciliateBillsApi
|
||||||
|
.getRecommendUser({
|
||||||
|
paymentId: row.paymentId,
|
||||||
|
come: row.come,
|
||||||
|
orderType: row.orderType,
|
||||||
|
courseId: row.courseId,
|
||||||
|
vipType: row.vipType ?? '',
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
(data) =>
|
||||||
|
data.list.map((item: RecommendedUser) => ({
|
||||||
|
value: item.tel,
|
||||||
|
})) || [],
|
||||||
|
),
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
field: 'tel',
|
||||||
|
title: '用户手机号',
|
||||||
|
},
|
||||||
|
{ editRender: { name: 'Input' }, field: 'orderMoney', title: '订单金额' },
|
||||||
|
{ editRender: { name: 'Input' }, field: 'realMoney', title: '实际金额' },
|
||||||
|
{
|
||||||
|
editRender: {
|
||||||
|
name: 'DatePicker',
|
||||||
|
props: {
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 仅VIP 和课程订单 显示
|
||||||
|
show: (row: CreateOrderType) => row.orderType === '1' || row.orderType === '2',
|
||||||
|
field: 'startTime',
|
||||||
|
title: '开始时间',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
editRender: {
|
||||||
|
name: 'Select',
|
||||||
|
props: {
|
||||||
|
options: [
|
||||||
|
{ label: '半年', value: '6' },
|
||||||
|
{ label: '一年', value: '12' },
|
||||||
|
{ label: '两年', value: '24' },
|
||||||
|
{ label: '三年', value: '36' },
|
||||||
|
{ label: '四年', value: '48' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 仅VIP 和课程订单 显示
|
||||||
|
show: (row: CreateOrderType) => row.orderType === '1' || row.orderType === '2',
|
||||||
|
field: 'endTime',
|
||||||
|
title: '到期时间',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showTitle: true,
|
||||||
|
titleField: 'productName',
|
||||||
|
gridColumns: 4,
|
||||||
|
gridHeight: '355px',
|
||||||
|
cardHeight: '345px',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useCardList<CreateOrderType>({
|
||||||
|
gridOptions: gridOptions.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加选中项
|
||||||
|
function handleAdd(rows: CreateOrderType[]) {
|
||||||
|
gridApi.insertAt(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片删除触发
|
||||||
|
function handleDelete(row: CreateOrderType) {
|
||||||
|
emit('deletedChecked', [row.id]);
|
||||||
|
}
|
||||||
|
// 删除数据
|
||||||
|
function deleteData(id: number) {
|
||||||
|
const index = getData().findIndex((item) => item.id === id);
|
||||||
|
gridApi.remove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空已选
|
||||||
|
function clearData() {
|
||||||
|
gridApi.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getData() {
|
||||||
|
return gridApi.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getData,
|
||||||
|
handleAdd,
|
||||||
|
handleDelete,
|
||||||
|
clearData,
|
||||||
|
deleteData,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Grid>
|
||||||
|
<template #card-extra="{ row }">
|
||||||
|
<Button type="link" size="small" danger @click.stop="handleDelete(row)">删除</Button>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CreateOrderType, PaymentRowType } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
import { useCardListForm } from '../composables/useCardListForm';
|
||||||
|
import { useCardListGrid } from '../composables/useCardListGrid';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
payment: PaymentRowType | undefined;
|
||||||
|
selectedData: CreateOrderType[] | undefined;
|
||||||
|
tabKey: string;
|
||||||
|
}>(),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['completeCheck', 'deletedChecked']);
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
productId: number;
|
||||||
|
year: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformData(rows: RowType[]) {
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.productId,
|
||||||
|
courseId: '',
|
||||||
|
tel: '',
|
||||||
|
come: '2',
|
||||||
|
orderType: '4',
|
||||||
|
paymentId: props.payment?.id || '',
|
||||||
|
productName: row.title,
|
||||||
|
productId: row.productId,
|
||||||
|
catalogueId: '',
|
||||||
|
orderMoney: '',
|
||||||
|
realMoney: '',
|
||||||
|
districtMoney: '',
|
||||||
|
startTime: props.payment?.ctime || '',
|
||||||
|
endTime: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formOptions } = useCardListForm({
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { allowClear: true },
|
||||||
|
fieldName: 'title',
|
||||||
|
defaultValue: '',
|
||||||
|
label: '培训班名称',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
|
||||||
|
payment: computed(() => props.payment),
|
||||||
|
selectedData: computed(() => props.selectedData),
|
||||||
|
rowKey: 'productId',
|
||||||
|
rowKeyPrefix: props.tabKey,
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'title', title: '培训班名称', minWidth: 200 },
|
||||||
|
{ field: 'year', title: '年份', minWidth: 100 },
|
||||||
|
],
|
||||||
|
query: (params) =>
|
||||||
|
reconciliateBillsApi.getTrainingClassList({
|
||||||
|
...params,
|
||||||
|
paymentId: props.payment?.id || 0,
|
||||||
|
}),
|
||||||
|
transformData,
|
||||||
|
emit,
|
||||||
|
formOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ cancelCheck, setChecked });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="orders-container h-full">
|
||||||
|
<Grid />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CreateOrderType, PaymentRowType } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
import { useCardListGrid } from '../composables/useCardListGrid';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
payment: PaymentRowType | undefined;
|
||||||
|
selectedData: CreateOrderType[] | undefined;
|
||||||
|
tabKey: string;
|
||||||
|
}>(),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['completeCheck', 'deletedChecked']);
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformData(rows: RowType[]) {
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.type,
|
||||||
|
courseId: '',
|
||||||
|
tel: '',
|
||||||
|
come: '2',
|
||||||
|
orderType: '1',
|
||||||
|
paymentId: props.payment?.id || '',
|
||||||
|
productName: row.title,
|
||||||
|
productId: '',
|
||||||
|
catalogueId: '',
|
||||||
|
orderMoney: '',
|
||||||
|
realMoney: '',
|
||||||
|
districtMoney: '',
|
||||||
|
startTime: props.payment?.ctime || '',
|
||||||
|
endTime: '',
|
||||||
|
vipType: row.type,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
|
||||||
|
payment: computed(() => props.payment),
|
||||||
|
selectedData: computed(() => props.selectedData),
|
||||||
|
rowKey: 'type',
|
||||||
|
rowKeyPrefix: props.tabKey,
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'title', title: 'VIP名称', minWidth: 150 },
|
||||||
|
],
|
||||||
|
query: (params) =>
|
||||||
|
reconciliateBillsApi.getVipList({
|
||||||
|
...params,
|
||||||
|
paymentId: props.payment?.id || 0,
|
||||||
|
}),
|
||||||
|
transformData,
|
||||||
|
emit,
|
||||||
|
gridOptions: {
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ cancelCheck, setChecked });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="orders-container h-full">
|
||||||
|
<Grid />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CreateOrderType, PaymentRowType } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
import { useCardListForm } from '../composables/useCardListForm';
|
||||||
|
import { useCardListGrid } from '../composables/useCardListGrid';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
payment: PaymentRowType | undefined;
|
||||||
|
selectedData: CreateOrderType[] | undefined;
|
||||||
|
tabKey: string;
|
||||||
|
}>(),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['completeCheck', 'deletedChecked']);
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
courseId: number;
|
||||||
|
productId: number;
|
||||||
|
price: number;
|
||||||
|
productName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformData(rows: RowType[]) {
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.productId,
|
||||||
|
courseId: row.courseId || '',
|
||||||
|
tel: '',
|
||||||
|
come: '1',
|
||||||
|
orderType: '2',
|
||||||
|
paymentId: props.payment?.id || '',
|
||||||
|
productName: row.productName,
|
||||||
|
productId: row.productId,
|
||||||
|
catalogueId: '',
|
||||||
|
orderMoney: String(row.price),
|
||||||
|
realMoney: '',
|
||||||
|
districtMoney: '',
|
||||||
|
startTime: props.payment?.ctime || '',
|
||||||
|
endTime: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formOptions } = useCardListForm({
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { allowClear: true },
|
||||||
|
fieldName: 'title',
|
||||||
|
defaultValue: '',
|
||||||
|
label: '商品名称',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
|
||||||
|
payment: computed(() => props.payment),
|
||||||
|
selectedData: computed(() => props.selectedData),
|
||||||
|
rowKey: 'productId',
|
||||||
|
rowKeyPrefix: props.tabKey,
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'productName', title: '商品名称', minWidth: 100 },
|
||||||
|
{ field: 'price', title: '金额', minWidth: 100 },
|
||||||
|
],
|
||||||
|
query: reconciliateBillsApi.getWumenCourseBuyList,
|
||||||
|
transformData,
|
||||||
|
emit,
|
||||||
|
formOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ cancelCheck, setChecked });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="orders-container h-full">
|
||||||
|
<Grid />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CreateOrderType, PaymentRowType } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
import { useCardListGrid } from '../composables/useCardListGrid';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
payment: PaymentRowType | undefined;
|
||||||
|
selectedData: CreateOrderType[] | undefined;
|
||||||
|
tabKey: string;
|
||||||
|
}>(),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['completeCheck', 'deletedChecked']);
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
id: number;
|
||||||
|
point: number;
|
||||||
|
tel: string;
|
||||||
|
note: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformData(rows: RowType[]) {
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
courseId: '',
|
||||||
|
tel: row.tel,
|
||||||
|
come: '1',
|
||||||
|
orderType: '0',
|
||||||
|
paymentId: props.payment?.id || '',
|
||||||
|
productName: row.note,
|
||||||
|
productId: '',
|
||||||
|
catalogueId: '',
|
||||||
|
orderMoney: String(row.point),
|
||||||
|
realMoney: '',
|
||||||
|
districtMoney: '',
|
||||||
|
startTime: '',
|
||||||
|
endTime: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
|
||||||
|
payment: computed(() => props.payment),
|
||||||
|
selectedData: computed(() => props.selectedData),
|
||||||
|
rowKey: 'id',
|
||||||
|
rowKeyPrefix: props.tabKey,
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'note', title: '备注', minWidth: 150 },
|
||||||
|
{ field: 'point', title: '积分', minWidth: 100 },
|
||||||
|
{ field: 'tel', title: '手机号', minWidth: 150 },
|
||||||
|
],
|
||||||
|
query: (params) =>
|
||||||
|
reconciliateBillsApi.getWumenPointBuyList({
|
||||||
|
...params,
|
||||||
|
paymentId: props.payment?.id || 0,
|
||||||
|
}),
|
||||||
|
transformData,
|
||||||
|
emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ cancelCheck, setChecked });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="orders-container h-full">
|
||||||
|
<Grid />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CreateOrderType, PaymentRowType } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
import { useCardListGrid } from '../composables/useCardListGrid';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
payment: PaymentRowType | undefined;
|
||||||
|
selectedData: CreateOrderType[] | undefined;
|
||||||
|
tabKey: string;
|
||||||
|
}>(),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['completeCheck', 'deletedChecked']);
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
ucbId: number;
|
||||||
|
productId: number;
|
||||||
|
courseId: number;
|
||||||
|
catalogueId: number;
|
||||||
|
productName: string;
|
||||||
|
price: number;
|
||||||
|
tel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformData(rows: RowType[]) {
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.ucbId,
|
||||||
|
paymentId: props.payment?.id || 0,
|
||||||
|
courseId: row.courseId,
|
||||||
|
tel: row.tel,
|
||||||
|
come: '1',
|
||||||
|
orderType: '2',
|
||||||
|
productName: row.productName,
|
||||||
|
productId: '',
|
||||||
|
catalogueId: row.catalogueId,
|
||||||
|
orderMoney: '',
|
||||||
|
realMoney: '',
|
||||||
|
districtMoney: '',
|
||||||
|
startTime: props.payment?.ctime || '',
|
||||||
|
endTime: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
|
||||||
|
payment: computed(() => props.payment),
|
||||||
|
selectedData: computed(() => props.selectedData),
|
||||||
|
rowKey: 'ucbId',
|
||||||
|
rowKeyPrefix: props.tabKey,
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'productName', title: '课程名称', minWidth: 100 },
|
||||||
|
{ field: 'price', title: '金额', minWidth: 100 },
|
||||||
|
{ field: 'tel', title: '手机号', minWidth: 150 },
|
||||||
|
],
|
||||||
|
query: (params) =>
|
||||||
|
reconciliateBillsApi.getWumenRecommendCourseBuyList({
|
||||||
|
...params,
|
||||||
|
paymentId: props.payment?.id || 0,
|
||||||
|
}),
|
||||||
|
transformData,
|
||||||
|
emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ cancelCheck, setChecked });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="orders-container h-full">
|
||||||
|
<Grid />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CreateOrderType, PaymentRowType } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
import { useCardListForm } from '../composables/useCardListForm';
|
||||||
|
import { useCardListGrid } from '../composables/useCardListGrid';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
payment: PaymentRowType | undefined;
|
||||||
|
selectedData: CreateOrderType[] | undefined;
|
||||||
|
tabKey: string;
|
||||||
|
}>(),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['completeCheck', 'deletedChecked']);
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
id: number;
|
||||||
|
price: number;
|
||||||
|
productName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformData(rows: RowType[]) {
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
courseId: '',
|
||||||
|
tel: '',
|
||||||
|
come: '1',
|
||||||
|
orderType: '3',
|
||||||
|
paymentId: props.payment?.id || '',
|
||||||
|
productName: row.productName,
|
||||||
|
productId: row.id,
|
||||||
|
catalogueId: '',
|
||||||
|
orderMoney: String(row.price),
|
||||||
|
realMoney: '',
|
||||||
|
districtMoney: '',
|
||||||
|
startTime: '',
|
||||||
|
endTime: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formOptions } = useCardListForm({
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { allowClear: true },
|
||||||
|
fieldName: 'productName',
|
||||||
|
defaultValue: '',
|
||||||
|
label: '商品名称',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
|
||||||
|
payment: computed(() => props.payment),
|
||||||
|
selectedData: computed(() => props.selectedData),
|
||||||
|
rowKey: 'id',
|
||||||
|
rowKeyPrefix: props.tabKey,
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'productName', title: '商品名称', minWidth: 150 },
|
||||||
|
{ field: 'price', title: '金额', minWidth: 100 },
|
||||||
|
],
|
||||||
|
query: reconciliateBillsApi.getWumenPhysicalBuyList,
|
||||||
|
transformData,
|
||||||
|
emit,
|
||||||
|
formOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ cancelCheck, setChecked });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="orders-container h-full">
|
||||||
|
<Grid />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CreateOrderType, PaymentRowType } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
import { useCardListForm } from '../composables/useCardListForm';
|
||||||
|
import { useCardListGrid } from '../composables/useCardListGrid';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
payment: PaymentRowType | undefined;
|
||||||
|
selectedData: CreateOrderType[] | undefined;
|
||||||
|
tabKey: string;
|
||||||
|
}>(),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['completeCheck', 'deletedChecked']);
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
price: string;
|
||||||
|
courseId: string;
|
||||||
|
productName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformData(rows: RowType[]) {
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.courseId,
|
||||||
|
courseId: row.courseId || '',
|
||||||
|
tel: '',
|
||||||
|
come: '0',
|
||||||
|
orderType: '2',
|
||||||
|
paymentId: props.payment?.id || '',
|
||||||
|
productName: row.productName,
|
||||||
|
productId: '',
|
||||||
|
catalogueId: '',
|
||||||
|
orderMoney: '',
|
||||||
|
realMoney: '',
|
||||||
|
districtMoney: '',
|
||||||
|
startTime: props.payment?.ctime || '',
|
||||||
|
endTime: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formOptions } = useCardListForm({
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { allowClear: true },
|
||||||
|
fieldName: 'title',
|
||||||
|
defaultValue: '',
|
||||||
|
label: '商品名称',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
|
||||||
|
payment: computed(() => props.payment),
|
||||||
|
selectedData: computed(() => props.selectedData),
|
||||||
|
rowKey: 'courseId',
|
||||||
|
rowKeyPrefix: props.tabKey,
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'productName', title: '商品名称', minWidth: 150 },
|
||||||
|
{ field: 'price', title: '金额', minWidth: 100 },
|
||||||
|
],
|
||||||
|
query: reconciliateBillsApi.getYiluCourseBuyList,
|
||||||
|
transformData,
|
||||||
|
emit,
|
||||||
|
formOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ cancelCheck, setChecked });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="orders-container h-full">
|
||||||
|
<Grid />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CreateOrderType, PaymentRowType } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
import { useCardListGrid } from '../composables/useCardListGrid';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
payment: PaymentRowType | undefined;
|
||||||
|
selectedData: CreateOrderType[] | undefined;
|
||||||
|
tabKey: string;
|
||||||
|
}>(),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['completeCheck', 'deletedChecked']);
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
id: number;
|
||||||
|
point: number;
|
||||||
|
tel: string;
|
||||||
|
note: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformData(rows: RowType[]) {
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
courseId: '',
|
||||||
|
tel: row.tel,
|
||||||
|
come: '0',
|
||||||
|
orderType: '0',
|
||||||
|
paymentId: props.payment?.id || '',
|
||||||
|
productName: row.note,
|
||||||
|
productId: '',
|
||||||
|
catalogueId: '',
|
||||||
|
orderMoney: String(row.point),
|
||||||
|
realMoney: '',
|
||||||
|
districtMoney: '',
|
||||||
|
startTime: props.payment?.ctime || '',
|
||||||
|
endTime: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
|
||||||
|
payment: computed(() => props.payment),
|
||||||
|
selectedData: computed(() => props.selectedData),
|
||||||
|
rowKey: 'id',
|
||||||
|
rowKeyPrefix: props.tabKey,
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'note', title: '备注', minWidth: 150 },
|
||||||
|
{ field: 'point', title: '充值金额', minWidth: 100 },
|
||||||
|
{ field: 'tel', title: '手机号', minWidth: 150 },
|
||||||
|
],
|
||||||
|
query: (params) =>
|
||||||
|
reconciliateBillsApi.getYiluPointBuyList({
|
||||||
|
...params,
|
||||||
|
paymentId: props.payment?.id || 0,
|
||||||
|
}),
|
||||||
|
transformData,
|
||||||
|
emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ cancelCheck, setChecked });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="orders-container h-full">
|
||||||
|
<Grid />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CreateOrderType, PaymentRowType } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
import { useCardListGrid } from '../composables/useCardListGrid';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
payment: PaymentRowType | undefined;
|
||||||
|
selectedData: CreateOrderType[] | undefined;
|
||||||
|
tabKey: string;
|
||||||
|
}>(),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['completeCheck', 'deletedChecked']);
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
courseFee: string;
|
||||||
|
courseId: string;
|
||||||
|
studyDays: number;
|
||||||
|
cellPhone: string;
|
||||||
|
productName: string;
|
||||||
|
createDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformData(rows: RowType[]) {
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.courseId,
|
||||||
|
paymentId: props.payment?.id || 0,
|
||||||
|
courseId: row.courseId,
|
||||||
|
tel: row.cellPhone,
|
||||||
|
come: '0',
|
||||||
|
orderType: '2',
|
||||||
|
productName: row.productName,
|
||||||
|
productId: '',
|
||||||
|
catalogueId: '',
|
||||||
|
orderMoney: '',
|
||||||
|
realMoney: '',
|
||||||
|
districtMoney: '',
|
||||||
|
startTime: row.createDate,
|
||||||
|
endTime: String(row.studyDays),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
|
||||||
|
payment: computed(() => props.payment),
|
||||||
|
selectedData: computed(() => props.selectedData),
|
||||||
|
rowKey: 'courseId',
|
||||||
|
rowKeyPrefix: props.tabKey,
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'productName', title: '课程名称', minWidth: 150 },
|
||||||
|
{ field: 'courseFee', title: '金额', minWidth: 100 },
|
||||||
|
{ field: 'cellPhone', title: '手机号', minWidth: 150 },
|
||||||
|
{ field: 'studyDays', title: '学习天数', minWidth: 100 },
|
||||||
|
],
|
||||||
|
query: (params) =>
|
||||||
|
reconciliateBillsApi.getYiluRecommendCourseBuyList({
|
||||||
|
...params,
|
||||||
|
paymentId: props.payment?.id || 0,
|
||||||
|
}),
|
||||||
|
transformData,
|
||||||
|
emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ cancelCheck, setChecked });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="orders-container h-full">
|
||||||
|
<Grid />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CreateOrderType, PaymentRowType } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting';
|
||||||
|
|
||||||
|
import { useCardListForm } from '../composables/useCardListForm';
|
||||||
|
import { useCardListGrid } from '../composables/useCardListGrid';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
payment: PaymentRowType | undefined;
|
||||||
|
selectedData: CreateOrderType[] | undefined;
|
||||||
|
tabKey: string;
|
||||||
|
}>(),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['completeCheck', 'deletedChecked']);
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
id: string;
|
||||||
|
price: number;
|
||||||
|
productName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformData(rows: RowType[]) {
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
courseId: '',
|
||||||
|
tel: '',
|
||||||
|
come: '0',
|
||||||
|
orderType: '3',
|
||||||
|
paymentId: props.payment?.id || '',
|
||||||
|
productName: row.productName,
|
||||||
|
productId: row.id,
|
||||||
|
catalogueId: '',
|
||||||
|
orderMoney: String(row.price),
|
||||||
|
realMoney: '',
|
||||||
|
districtMoney: '',
|
||||||
|
startTime: props.payment?.ctime || '',
|
||||||
|
endTime: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formOptions } = useCardListForm({
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { allowClear: true },
|
||||||
|
fieldName: 'productName',
|
||||||
|
defaultValue: '',
|
||||||
|
label: '商品名称',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
|
||||||
|
payment: computed(() => props.payment),
|
||||||
|
selectedData: computed(() => props.selectedData),
|
||||||
|
rowKey: 'id',
|
||||||
|
rowKeyPrefix: props.tabKey,
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'productName', title: '商品名称', minWidth: 150 },
|
||||||
|
{ field: 'price', title: '金额', minWidth: 100 },
|
||||||
|
],
|
||||||
|
query: (params) =>
|
||||||
|
reconciliateBillsApi.getPhysicalBuyList({
|
||||||
|
...params,
|
||||||
|
paymentId: props.payment?.id || 0,
|
||||||
|
}),
|
||||||
|
transformData,
|
||||||
|
emit,
|
||||||
|
formOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ cancelCheck, setChecked });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="orders-container h-full">
|
||||||
|
<Grid />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
export function useCardListForm(userFormOptions?: VbenFormProps) {
|
||||||
|
const defaultFormOptions: VbenFormProps = {
|
||||||
|
collapsed: false,
|
||||||
|
wrapperClass: 'grid-cols-4',
|
||||||
|
showCollapseButton: false,
|
||||||
|
submitOnChange: true,
|
||||||
|
submitOnEnter: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const formOptions: VbenFormProps = {
|
||||||
|
...defaultFormOptions,
|
||||||
|
...userFormOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { formOptions };
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { VxeGridListeners, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
interface UseCardListGridOptions<RowType, OrderType> {
|
||||||
|
payment: Ref<any>;
|
||||||
|
selectedData: Ref<OrderType[] | undefined>;
|
||||||
|
rowKey: string;
|
||||||
|
rowKeyPrefix?: string;
|
||||||
|
columns: VxeTableGridOptions<RowType>['columns'];
|
||||||
|
query: (params: any) => Promise<any>;
|
||||||
|
transformData: (row: RowType[]) => OrderType[];
|
||||||
|
emit: (event: 'completeCheck' | 'deletedChecked', payload: any) => void;
|
||||||
|
|
||||||
|
formOptions?: any;
|
||||||
|
gridOptions?: Partial<VxeTableGridOptions<RowType>>;
|
||||||
|
gridEvents?: VxeGridListeners<RowType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCardListGrid<RowType, OrderType>(
|
||||||
|
options: UseCardListGridOptions<RowType, OrderType>,
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
payment,
|
||||||
|
selectedData,
|
||||||
|
rowKey,
|
||||||
|
rowKeyPrefix,
|
||||||
|
columns,
|
||||||
|
query,
|
||||||
|
transformData,
|
||||||
|
emit,
|
||||||
|
formOptions,
|
||||||
|
gridOptions: userGridOptions,
|
||||||
|
gridEvents: userGridEvents,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
function setChecked(data: OrderType[]) {
|
||||||
|
const currentPageData = gridApi.grid.getData();
|
||||||
|
const rows = currentPageData.filter((row: any) =>
|
||||||
|
data.some((item: any) => item.id === row[rowKey]),
|
||||||
|
);
|
||||||
|
rows.length > 0 && gridApi.grid.setCheckboxRow(rows, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelCheck(id: number) {
|
||||||
|
gridApi.grid.setCheckboxRow({ [rowKey]: id }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultGridOptions: VxeTableGridOptions<RowType> = {
|
||||||
|
columns,
|
||||||
|
exportConfig: {},
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
rowConfig: {
|
||||||
|
keyField: rowKey,
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
checkboxConfig: {
|
||||||
|
reserve: true,
|
||||||
|
showReserveStatus: true,
|
||||||
|
highlight: true,
|
||||||
|
trigger: 'row',
|
||||||
|
},
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
response: {
|
||||||
|
result: 'records',
|
||||||
|
total: 'total',
|
||||||
|
list: 'records',
|
||||||
|
},
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
const data = await query({
|
||||||
|
page: page.currentPage,
|
||||||
|
limit: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
// 确保 listKey 是字符串类型
|
||||||
|
const responseList = userGridOptions?.proxyConfig?.response?.list;
|
||||||
|
const listKey = typeof responseList === 'string' ? responseList : 'records';
|
||||||
|
const list = data[listKey].map((item: any) => ({
|
||||||
|
...item,
|
||||||
|
[rowKey]: `${rowKeyPrefix || ''}${item[rowKey]}`,
|
||||||
|
}));
|
||||||
|
data[listKey] = list;
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
querySuccess: () => {
|
||||||
|
setChecked(selectedData.value || []);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalGridOptions: VxeTableGridOptions<RowType> = {
|
||||||
|
...defaultGridOptions,
|
||||||
|
...userGridOptions,
|
||||||
|
// 对 proxyConfig 进行特殊处理,确保用户配置能覆盖默认配置
|
||||||
|
proxyConfig: {
|
||||||
|
...defaultGridOptions.proxyConfig,
|
||||||
|
...userGridOptions?.proxyConfig,
|
||||||
|
// 对 proxyConfig.ajax 进行特殊处理
|
||||||
|
ajax: {
|
||||||
|
...defaultGridOptions.proxyConfig?.ajax,
|
||||||
|
...userGridOptions?.proxyConfig?.ajax,
|
||||||
|
},
|
||||||
|
// 对 proxyConfig.response 进行特殊处理
|
||||||
|
response: {
|
||||||
|
...defaultGridOptions.proxyConfig?.response,
|
||||||
|
...userGridOptions?.proxyConfig?.response,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultGridEvents: VxeGridListeners<RowType> = {
|
||||||
|
checkboxChange: ({ checked, row }: any) => {
|
||||||
|
checked ? emit('completeCheck', transformData([row])) : emit('deletedChecked', [row[rowKey]]);
|
||||||
|
},
|
||||||
|
checkboxAll: ({ checked }: any) => {
|
||||||
|
const rows = gridApi.grid.getData();
|
||||||
|
const ids = rows.map((item: any) => item[rowKey]);
|
||||||
|
checked ? emit('completeCheck', transformData(rows)) : emit('deletedChecked', ids);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalGridEvents: VxeGridListeners<RowType> = {
|
||||||
|
...defaultGridEvents,
|
||||||
|
...userGridEvents,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions,
|
||||||
|
gridOptions: finalGridOptions as any,
|
||||||
|
gridEvents: finalGridEvents,
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
payment,
|
||||||
|
(newPayment, oldPayment) => {
|
||||||
|
if (newPayment?.id && newPayment.id !== oldPayment?.id) {
|
||||||
|
gridApi?.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
Grid,
|
||||||
|
gridApi,
|
||||||
|
cancelCheck,
|
||||||
|
setChecked,
|
||||||
|
};
|
||||||
|
}
|
||||||
229
apps/finance/src/views/posting/reconciliate/index.vue
Normal file
229
apps/finance/src/views/posting/reconciliate/index.vue
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { PaymentRowType } from './types';
|
||||||
|
|
||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, message, notification, Tag } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting';
|
||||||
|
import { useSysStore } from '#/store';
|
||||||
|
|
||||||
|
import Manual from './modules/Manual.vue';
|
||||||
|
|
||||||
|
const sysStore = useSysStore();
|
||||||
|
|
||||||
|
const formOptions: VbenFormProps = {
|
||||||
|
// 默认展开
|
||||||
|
collapsed: false,
|
||||||
|
// 设置每行最多显示4个表单项
|
||||||
|
wrapperClass: 'grid-cols-6',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: false,
|
||||||
|
picker: 'month',
|
||||||
|
valueFormat: 'YYYY-MM',
|
||||||
|
},
|
||||||
|
defaultValue: dayjs().format('YYYY-MM'),
|
||||||
|
fieldName: 'date',
|
||||||
|
label: '日期',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: sysStore.getDictList('payment'),
|
||||||
|
placeholder: '请选择',
|
||||||
|
},
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '支付方式',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
optionType: 'button',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '全部',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
...sysStore.getDictList('checkoff'),
|
||||||
|
],
|
||||||
|
placeholder: '请选择',
|
||||||
|
},
|
||||||
|
defaultValue: '',
|
||||||
|
fieldName: 'checkoff',
|
||||||
|
label: '核对状态',
|
||||||
|
// 核对状态占1列
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 控制表单是否显示折叠按钮
|
||||||
|
showCollapseButton: false,
|
||||||
|
// 是否在字段值改变时提交表单
|
||||||
|
submitOnChange: true,
|
||||||
|
// 按下回车时是否提交表单
|
||||||
|
submitOnEnter: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridOptions: VxeTableGridOptions<PaymentRowType> = {
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
title: '支付方式',
|
||||||
|
minWidth: 80,
|
||||||
|
formatter: ({ cellValue }) => {
|
||||||
|
return sysStore.getDictMap('payment', cellValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ field: 'fee', title: '金额', minWidth: 100 },
|
||||||
|
{ field: 'ctime', title: '时间', minWidth: 100, formatter: 'formatDateTime' },
|
||||||
|
{ field: 'transactionSn', title: '业务流水号', minWidth: 150 },
|
||||||
|
{ field: 'financeSn', title: '财务流水号', minWidth: 150 },
|
||||||
|
{ field: 'relationSn', title: '关联sn号', minWidth: 160 },
|
||||||
|
{ field: 'remark', title: '备注', minWidth: 120 },
|
||||||
|
{
|
||||||
|
field: 'checkoff',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 100,
|
||||||
|
slots: { default: 'status' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'action' },
|
||||||
|
title: '操作',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exportConfig: {},
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
rowConfig: {
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
response: {
|
||||||
|
result: 'records',
|
||||||
|
total: 'total',
|
||||||
|
list: 'records',
|
||||||
|
},
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
const date = formValues.date.split('-');
|
||||||
|
delete formValues.date;
|
||||||
|
return await reconciliateBillsApi.getReconciliationList({
|
||||||
|
page: page.currentPage,
|
||||||
|
limit: page.pageSize,
|
||||||
|
year: date[0],
|
||||||
|
month: date[1],
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions,
|
||||||
|
gridOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onStartSysCheck() {
|
||||||
|
// 获取表单值
|
||||||
|
const formValues = await gridApi?.formApi?.getValues();
|
||||||
|
|
||||||
|
// 处理日期格式
|
||||||
|
if (formValues?.date) {
|
||||||
|
const date = formValues.date.split('-');
|
||||||
|
const params = {
|
||||||
|
year: date[0],
|
||||||
|
month: date[1],
|
||||||
|
};
|
||||||
|
// 调用自动核对接口
|
||||||
|
const hide = message.loading('系统自动核对中...', 0);
|
||||||
|
await reconciliateBillsApi.autoCheck(params);
|
||||||
|
hide();
|
||||||
|
gridApi?.query();
|
||||||
|
notification.success({
|
||||||
|
message: '完成',
|
||||||
|
description: '系统自动核对完成,核对失败的单据请人工核对。',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectData = ref<PaymentRowType[]>([]);
|
||||||
|
const manualDialogVisible = ref(false);
|
||||||
|
|
||||||
|
// 监听弹框关闭,刷新列表数据
|
||||||
|
watch(manualDialogVisible, (newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
// 弹框关闭时刷新列表(保持在当前页)
|
||||||
|
gridApi?.query();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
async function onManualCheck(data?: PaymentRowType) {
|
||||||
|
// 如果没有传入数据,则获取选中的行
|
||||||
|
if (data) {
|
||||||
|
selectData.value = [data];
|
||||||
|
} else {
|
||||||
|
const selectedRows = await gridApi?.grid?.getCheckboxRecords();
|
||||||
|
if (selectedRows.length === 0) {
|
||||||
|
message.error('请先选择数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectData.value = selectedRows;
|
||||||
|
}
|
||||||
|
// 处理selectData,checkoff状态为1才需要人工核对
|
||||||
|
const originalLength = selectData.value.length;
|
||||||
|
selectData.value = selectData.value.filter((item: PaymentRowType) => item.checkoff === 1);
|
||||||
|
const filteredCount = originalLength - selectData.value.length;
|
||||||
|
|
||||||
|
// 如果有数据被过滤掉,显示警告
|
||||||
|
if (filteredCount > 0) {
|
||||||
|
message.warning(`已过滤掉 ${filteredCount} 条不符合条件的数据`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectData.value.length === 0) {
|
||||||
|
message.error('选择的数据中没有可进行人工对账的数据');
|
||||||
|
} else {
|
||||||
|
manualDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<Grid>
|
||||||
|
<template #toolbar-actions>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button type="primary" @click="onStartSysCheck()">启动系统自动对账</Button>
|
||||||
|
<Button type="primary" @click="onManualCheck()">人工对账</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<Button v-if="row.checkoff === 1" type="primary" size="small" @click="onManualCheck(row)">
|
||||||
|
人工对账
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template #status="{ row }">
|
||||||
|
<Tag :color="sysStore.getDictByList('checkoff', row.checkoff).color || 'info'">
|
||||||
|
{{ sysStore.getDictMap('checkoff', row.checkoff) }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
<Manual v-if="manualDialogVisible" v-model:show="manualDialogVisible" :data="selectData" />
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
331
apps/finance/src/views/posting/reconciliate/modules/Manual.vue
Normal file
331
apps/finance/src/views/posting/reconciliate/modules/Manual.vue
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CreateOrderType, PaymentRowType } from '../types';
|
||||||
|
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Button, Card, message, Modal, notification } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { reconciliateBillsApi } from '#/api/posting/reconciliate';
|
||||||
|
import { useSysStore } from '#/store';
|
||||||
|
|
||||||
|
import Orders from '../components/Orders.vue';
|
||||||
|
import Selected from '../components/Selected.vue';
|
||||||
|
import TrainingClassProduct from '../components/TrainingClassProduct.vue';
|
||||||
|
import VipProduct from '../components/VipProduct.vue';
|
||||||
|
import WumenCourseProduct from '../components/WumenCourseProduct.vue';
|
||||||
|
import WumenCourseRecharge from '../components/WumenCourseRecharge.vue';
|
||||||
|
import WumenCourseRecord from '../components/WumenCourseRecord.vue';
|
||||||
|
import WumenPhysicalProduct from '../components/WumenPhysicalProduct.vue';
|
||||||
|
import YiluCourseProduct from '../components/YiluCourseProduct.vue';
|
||||||
|
import YiluCourseRecharge from '../components/YiluCourseRecharge.vue';
|
||||||
|
import YiluCourseRecord from '../components/YiluCourseRecord.vue';
|
||||||
|
import YiluPhysicalProduct from '../components/YiluPhysicalProduct.vue';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
data?: PaymentRowType[];
|
||||||
|
show?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
show: false,
|
||||||
|
data: () => [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:show']);
|
||||||
|
|
||||||
|
const needCreateRecord: Record<string, any> = {
|
||||||
|
A: WumenCourseRecord,
|
||||||
|
B: WumenCourseProduct,
|
||||||
|
C: WumenCourseRecharge,
|
||||||
|
D: WumenPhysicalProduct,
|
||||||
|
E: YiluCourseRecord,
|
||||||
|
F: YiluCourseProduct,
|
||||||
|
G: YiluCourseRecharge,
|
||||||
|
H: YiluPhysicalProduct,
|
||||||
|
I: VipProduct,
|
||||||
|
J: TrainingClassProduct,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sysStore = useSysStore();
|
||||||
|
|
||||||
|
const visible = ref(props.show);
|
||||||
|
watch(visible, (val) => {
|
||||||
|
emit('update:show', val);
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentIndex = ref(0);
|
||||||
|
const pendingData = ref<PaymentRowType[]>([]);
|
||||||
|
|
||||||
|
// 监听props.data变化,更新本地pendingData
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(newData) => {
|
||||||
|
pendingData.value = [...newData];
|
||||||
|
currentIndex.value = 0;
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
function changePendingData(index: number) {
|
||||||
|
currentIndex.value = index;
|
||||||
|
selectedData.value = [];
|
||||||
|
selectedRef.value?.clearData();
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabList = [
|
||||||
|
{
|
||||||
|
key: '0',
|
||||||
|
tab: '订单列表',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'A',
|
||||||
|
tab: '吴门医述开课记录',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'B',
|
||||||
|
tab: '吴门医述课程商品',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'C',
|
||||||
|
tab: '吴门医述充值记录',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'D',
|
||||||
|
tab: '吴门医述实物商品',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'E',
|
||||||
|
tab: '一路健康开课记录',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'F',
|
||||||
|
tab: '一路健康课程商品',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'G',
|
||||||
|
tab: '一路健康充值记录',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'H',
|
||||||
|
tab: '一路健康实物商品',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'I',
|
||||||
|
tab: '开通VIP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'J',
|
||||||
|
tab: '报名培训班',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const activeTabKey = ref('0');
|
||||||
|
function changeRecordTab(key: string) {
|
||||||
|
activeTabKey.value = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认对账完成
|
||||||
|
function onCompleteCheck() {
|
||||||
|
notification.success({
|
||||||
|
message: '操作成功',
|
||||||
|
});
|
||||||
|
// 删除当前已对账的数据
|
||||||
|
pendingData.value.splice(currentIndex.value, 1);
|
||||||
|
|
||||||
|
// 如果没有待对账数据了,关闭弹框
|
||||||
|
if (pendingData.value.length === 0) {
|
||||||
|
visible.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果当前索引超出了数据范围,重置到第一条
|
||||||
|
if (currentIndex.value >= pendingData.value.length) {
|
||||||
|
currentIndex.value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 其他方式--确认对账 --------------
|
||||||
|
// 已选列表
|
||||||
|
const selectedRef = ref();
|
||||||
|
const errorData = ref<CreateOrderType[]>([]);
|
||||||
|
const selectedData = ref<CreateOrderType[]>([]);
|
||||||
|
const needCreateRecordRef = ref();
|
||||||
|
// 选中
|
||||||
|
function onSelectRecord(rows: CreateOrderType[]) {
|
||||||
|
const addData = rows.filter(
|
||||||
|
(item) => !selectedData.value.some((selected) => selected.id === item.id),
|
||||||
|
);
|
||||||
|
selectedData.value.push(...addData);
|
||||||
|
selectedRef.value?.handleAdd(addData);
|
||||||
|
}
|
||||||
|
// 取消选中
|
||||||
|
function onDeleteChecked(ids: number[]) {
|
||||||
|
ids.forEach((id) => {
|
||||||
|
const rowData = selectedData.value.find((item) => item.id === id);
|
||||||
|
const index = selectedData.value.findIndex((item) => item.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
selectedData.value.splice(index, 1);
|
||||||
|
selectedRef.value?.deleteData(id);
|
||||||
|
}
|
||||||
|
if (rowData && needCreateRecordRef.value) {
|
||||||
|
needCreateRecordRef.value.cancelCheck(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 确认对账
|
||||||
|
function onCompleteCheckCreated() {
|
||||||
|
if (selectedData.value.length === 0) {
|
||||||
|
message.error('请选择数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selected = selectedRef.value.getData();
|
||||||
|
|
||||||
|
// 检查已选数据中是否有未填写的表单项
|
||||||
|
const emptyItem = selected.find((item: CreateOrderType) => {
|
||||||
|
if (
|
||||||
|
!item.realMoney ||
|
||||||
|
!item.orderMoney ||
|
||||||
|
!item.tel ||
|
||||||
|
((item.orderType === '1' || item.orderType === '2') && (!item.startTime || !item.endTime))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (emptyItem) {
|
||||||
|
errorData.value = [emptyItem];
|
||||||
|
message.error(`《${emptyItem.productName}》有未填写项`);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
errorData.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断已选的数据填写的实际金额总和是否与对账单金额一致
|
||||||
|
const totalRealMoney = selected.reduce(
|
||||||
|
(acc: number, item: CreateOrderType) => acc + Number.parseFloat(item.realMoney),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
if (totalRealMoney !== Number.parseFloat(pendingData.value?.[currentIndex.value]?.fee || '0')) {
|
||||||
|
message.error('已选数据填写的实际金额总和与对账单金额不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = selected.map((item: CreateOrderType) => {
|
||||||
|
const result: any = {
|
||||||
|
...item,
|
||||||
|
districtMoney: (
|
||||||
|
Number.parseFloat(item.orderMoney) - Number.parseFloat(item.realMoney)
|
||||||
|
).toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 只有VIP和课程订单才处理开始时间和结束时间
|
||||||
|
if (item.orderType === '1' || item.orderType === '2') {
|
||||||
|
result.startTime = dayjs(item.startTime).format('YYYY-MM-DD'); // 格式化开始时间
|
||||||
|
result.endTime = dayjs(item.startTime)
|
||||||
|
.add(Number.parseFloat(item.endTime), 'month')
|
||||||
|
.format('YYYY-MM-DD'); // 处理结束时间,根据传入的vb时间和课程时长计算
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
// 调用接口
|
||||||
|
reconciliateBillsApi.manualCheckCreated(data).then(() => {
|
||||||
|
onCompleteCheck();
|
||||||
|
// 清空已选列表
|
||||||
|
selectedData.value = [];
|
||||||
|
selectedRef.value?.clearData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
width="100%"
|
||||||
|
title="人工对账"
|
||||||
|
:footer="null"
|
||||||
|
centered
|
||||||
|
wrap-class-name="full-modal"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Card title="待对账列表" class="w-1/4" size="small">
|
||||||
|
<div class="flex flex-col gap-2 overflow-y-auto p-1" style="height: calc(100vh - 120px)">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in pendingData"
|
||||||
|
:key="item.id"
|
||||||
|
:class="{ active: currentIndex === index }"
|
||||||
|
class="pay-order-item"
|
||||||
|
@click="changePendingData(index)"
|
||||||
|
>
|
||||||
|
<div>关联sn号:{{ item.relationSn }}</div>
|
||||||
|
<div>业务流水号:{{ item.transactionSn }}</div>
|
||||||
|
<div>财务流水号:{{ item.financeSn }}</div>
|
||||||
|
<div>金额:{{ item.fee }}</div>
|
||||||
|
<div>支付方式:{{ sysStore.getDictMap('payment', item.type) }}</div>
|
||||||
|
<div>时间:{{ dayjs(item.ctime).format('YYYY-MM-DD HH:mm:ss') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<div class="flex flex-1 flex-col gap-2">
|
||||||
|
<Card
|
||||||
|
:tab-list="tabList"
|
||||||
|
:active-tab-key="activeTabKey"
|
||||||
|
@tab-change="changeRecordTab"
|
||||||
|
class="order-card flex-1"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<Orders
|
||||||
|
v-if="activeTabKey === '0'"
|
||||||
|
:payment-id="pendingData[currentIndex]?.id"
|
||||||
|
@complete-check="onCompleteCheck"
|
||||||
|
/>
|
||||||
|
<component
|
||||||
|
:is="needCreateRecord[activeTabKey]"
|
||||||
|
v-if="activeTabKey !== '0'"
|
||||||
|
ref="needCreateRecordRef"
|
||||||
|
:tab-key="activeTabKey"
|
||||||
|
:payment="pendingData[currentIndex]"
|
||||||
|
:selected-data="selectedData"
|
||||||
|
@complete-check="onSelectRecord"
|
||||||
|
@deleted-checked="onDeleteChecked"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
v-if="activeTabKey !== '0'"
|
||||||
|
:title="`已选列表(${selectedData.length})`"
|
||||||
|
class="h-[405px] w-full"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<Button type="primary" size="small" @click="onCompleteCheckCreated()">确认对账</Button>
|
||||||
|
</template>
|
||||||
|
<Selected ref="selectedRef" :error-item="errorData" @deleted-checked="onDeleteChecked" />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pay-order-item {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
padding: 9px;
|
||||||
|
border: 2px solid #1890ff;
|
||||||
|
background-color: rgba(24, 144, 255, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 5px !important;
|
||||||
|
}
|
||||||
|
.order-card {
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 1px !important;
|
||||||
|
height: calc(100% - 46px); // 减去标签页头部高度
|
||||||
|
// background-color: #f1f3f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
apps/finance/src/views/posting/reconciliate/types.d.ts
vendored
Normal file
29
apps/finance/src/views/posting/reconciliate/types.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// 实际支付记录
|
||||||
|
export interface PaymentRowType {
|
||||||
|
checkoff: number;
|
||||||
|
ctime: string;
|
||||||
|
fee: string;
|
||||||
|
financeSn: string;
|
||||||
|
id: number;
|
||||||
|
transactionSn: string;
|
||||||
|
relationSn: string;
|
||||||
|
type: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建订单参数
|
||||||
|
export interface CreateOrderType {
|
||||||
|
id: number | string;
|
||||||
|
paymentId: number | string;
|
||||||
|
courseId: number | string;
|
||||||
|
tel: string;
|
||||||
|
come: string;
|
||||||
|
orderType: string;
|
||||||
|
productName: string;
|
||||||
|
catalogueId: number | string;
|
||||||
|
orderMoney: string;
|
||||||
|
districtMoney: string;
|
||||||
|
realMoney: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
vipType?: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user