feat(财务): 新增天医币订单和用户余额模块
Some checks failed
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

- 添加天医币订单相关API和页面
- 添加用户余额相关API和页面
- 在系统字典中新增来源和订单类型配置
- 调整部分UI样式和表单配置
This commit is contained in:
2026-01-12 09:20:38 +08:00
parent f93751e74a
commit 963e2a8d39
14 changed files with 406 additions and 328 deletions

View File

@@ -1,3 +0,0 @@
1. 这是一个企业级项目,结构复杂,你需要深度检测代码后才能理解其业务逻辑,不能简单的只根据当前文件的代码来理解。
2. 代码修改完成后,必须检查文件内引用是否正确
3. 修改后,必须获取项目的诊断信息,了解是否有错误或警告,然后根据诊断信息进行修复。

View File

@@ -1,30 +0,0 @@
{
"recommendations": [
// Vue 3 的语言支持
"Vue.volar",
// 将 ESLint JavaScript 集成到 VS Code 中。
"dbaeumer.vscode-eslint",
// Visual Studio Code 的官方 Stylelint 扩展
"stylelint.vscode-stylelint",
// 使用 Prettier 的代码格式化程序
"esbenp.prettier-vscode",
// 支持 dotenv 文件语法
"mikestead.dotenv",
// 源代码的拼写检查器
"streetsidesoftware.code-spell-checker",
// Tailwind CSS 的官方 VS Code 插件
"bradlc.vscode-tailwindcss",
// iconify 图标插件
"antfu.iconify",
// i18n 插件
"Lokalise.i18n-ally",
// CSS 变量提示
"vunguyentuan.vscode-css-variables",
// 在 package.json 中显示 PNPM catalog 的版本
"antfu.pnpm-catalog-lens"
],
"unwantedRecommendations": [
// 和 volar 冲突
"octref.vetur"
]
}

View File

@@ -1,37 +0,0 @@
{
"import": {
"scope": "javascript,typescript",
"prefix": "im",
"body": ["import { $2 } from '$1';"],
"description": "Import a module",
},
"export-all": {
"scope": "javascript,typescript",
"prefix": "ex",
"body": ["export * from '$1';"],
"description": "Export a module",
},
"vue-script-setup": {
"scope": "vue",
"prefix": "<sc",
"body": [
"<script setup lang=\"ts\">",
"const props = defineProps<{",
" modelValue?: boolean,",
"}>()",
"$1",
"</script>",
"",
"<template>",
" <div>",
" <slot/>",
" </div>",
"</template>",
],
},
"vue-computed": {
"scope": "javascript,typescript,vue",
"prefix": "com",
"body": ["computed(() => { $1 })"],
},
}

15
.vscode/launch.json vendored
View File

@@ -1,15 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"name": "finance admin dev",
"request": "launch",
"url": "http://localhost:9000",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}/apps/finance"
}
]
}

241
.vscode/settings.json vendored
View File

@@ -1,241 +0,0 @@
{
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts",
// workbench
"workbench.list.smoothScrolling": true,
"workbench.startupEditor": "newUntitledFile",
"workbench.tree.indent": 10,
"workbench.editor.highlightModifiedTabs": true,
"workbench.editor.closeOnFileDelete": true,
"workbench.editor.limit.enabled": true,
"workbench.editor.limit.perEditorGroup": true,
"workbench.editor.limit.value": 8,
// editor
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.cursorBlinking": "expand",
"editor.largeFileOptimizations": true,
"editor.accessibilitySupport": "off",
"editor.cursorSmoothCaretAnimation": "on",
"editor.guides.bracketPairs": "active",
"editor.inlineSuggest.enabled": true,
"editor.suggestSelection": "recentlyUsedByPrefix",
"editor.acceptSuggestionOnEnter": "smart",
"editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.stickyScroll.enabled": true,
"editor.hover.sticky": true,
"editor.suggest.insertMode": "replace",
"editor.bracketPairColorization.enabled": true,
"editor.autoClosingBrackets": "beforeWhitespace",
"editor.autoClosingDelete": "always",
"editor.autoClosingOvertype": "always",
"editor.autoClosingQuotes": "beforeWhitespace",
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit",
"source.organizeImports": "never"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// extensions
"extensions.ignoreRecommendations": true,
// terminal
"terminal.integrated.cursorBlinking": true,
"terminal.integrated.persistentSessionReviveProcess": "never",
"terminal.integrated.tabs.enabled": true,
"terminal.integrated.scrollback": 10000,
"terminal.integrated.stickyScroll.enabled": true,
// files
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.simpleDialog.enable": true,
"files.associations": {
"*.ejs": "html",
"*.art": "html",
"**/tsconfig.json": "jsonc",
"*.json": "jsonc",
"package.json": "json"
},
"files.exclude": {
"**/.eslintcache": true,
"**/bower_components": true,
"**/.turbo": true,
"**/.idea": true,
"**/.vitepress": true,
"**/tmp": true,
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.stylelintcache": true,
"**/.DS_Store": true,
"**/vite.config.mts.*": true,
"**/tea.yaml": true
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/.vscode/**": true,
"**/node_modules/**": true,
"**/tmp/**": true,
"**/bower_components/**": true,
"**/dist/**": true,
"**/yarn.lock": true
},
"typescript.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"],
// search
"search.searchEditor.singleClickBehaviour": "peekDefinition",
"search.followSymlinks": false,
// 在使用搜索功能时,将这些文件夹/文件排除在外
"search.exclude": {
"**/node_modules": true,
"**/*.log": true,
"**/*.log*": true,
"**/bower_components": true,
"**/dist": true,
"**/elehukouben": true,
"**/.git": true,
"**/.github": true,
"**/.gitignore": true,
"**/.svn": true,
"**/.DS_Store": true,
"**/.vitepress/cache": true,
"**/.idea": true,
"**/.vscode": false,
"**/.yarn": true,
"**/tmp": true,
"*.xml": true,
"out": true,
"dist": true,
"node_modules": true,
"CHANGELOG.md": true,
"**/pnpm-lock.yaml": true,
"**/yarn.lock": true
},
"debug.onTaskErrors": "debugAnyway",
"diffEditor.ignoreTrimWhitespace": false,
"npm.packageManager": "pnpm",
"css.validate": false,
"less.validate": false,
"scss.validate": false,
// extension
"emmet.showSuggestionsAsSnippets": true,
"emmet.triggerExpansionOnTab": false,
"errorLens.enabledDiagnosticLevels": ["warning", "error"],
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"],
"stylelint.enable": true,
"stylelint.packageManager": "pnpm",
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"],
"stylelint.customSyntax": "postcss-html",
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
"typescript.inlayHints.enumMemberValues.enabled": true,
"typescript.preferences.preferTypeOnlyAutoImports": true,
"typescript.preferences.includePackageJsonAutoImports": "on",
"eslint.validate": [
"javascript",
"typescript",
"javascriptreact",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"json5"
],
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
],
"github.copilot.enable": {
"*": true,
"markdown": true,
"plaintext": false,
"yaml": false
},
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"],
"i18n-ally.localesPaths": [
"packages/locales/src/langs",
"playground/src/locales/langs",
"apps/*/src/locales/langs"
],
"i18n-ally.pathMatcher": "{locale}/{namespace}.{ext}",
"i18n-ally.enabledParsers": ["json"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": true,
// 控制相关文件嵌套展示
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx, $(capture).d.ts",
"*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx,$(capture).d.ts",
"*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
"tailwind.config.mjs": "postcss.*"
},
"commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib",
"oxc.enable": false,
"cSpell.words": [
"archiver",
"axios",
"dotenv",
"isequal",
"jspm",
"napi",
"nolebase",
"rollup",
"vitest"
]
}

View File

@@ -1,2 +1,4 @@
export * from './import';
export * from './reconciliate';
export * from './surplus';
export * from './tianyibi';

View File

@@ -0,0 +1,10 @@
import { requestClient } from '#/api/request';
export const balanceApi = {
/**
* 获取用户余额列表
*/
getUserBalanceList: (data: { limit: number; page: number; tel?: string }) => {
return requestClient.post('/common/user/getUserList', data);
},
};

View File

@@ -0,0 +1,37 @@
import { requestClient } from '#/api/request';
export const tianyibiApi = {
/**
* 获取天医币订单列表
*/
getPointOrdersList: (data: {
endTime?: string;
limit: number;
page: number;
startTime?: string;
tel?: string;
}) => {
return requestClient.post('/common/orders/getPointOrdersListNoUse', data);
},
/**
* 手动确认消耗
*/
manualConsumeTianyibi: (data: { orderId: number }) => {
return requestClient.post('/common/orders/manualConsumePoint', data);
},
/**
* 取消消耗
*/
cancelConsumeTianyibi: (data: { orderId: number }) => {
return requestClient.post('/common/orders/cancelConsumePoint', data);
},
/**
* 自动消耗
*/
autoConsumeTianyibi: () => {
return requestClient.post('/common/orders/autoConsumePoint');
},
};

View File

@@ -29,6 +29,24 @@ const routes: RouteRecordRaw[] = [
path: '/posting/reconciliate-bills',
component: () => import('#/views/posting/reconciliate/index.vue'),
},
{
meta: {
title: '天医币订单',
keepAlive: true,
},
name: 'TianyibiOrders',
path: '/posting/tianyibi-orders',
component: () => import('#/views/posting/tianyibiOrders/index.vue'),
},
{
meta: {
title: '用户余额',
keepAlive: true,
},
name: 'UserSurplus',
path: '/posting/user-surplus',
component: () => import('#/views/posting/userSurplus/index.vue'),
},
],
},
];

View File

@@ -12,6 +12,17 @@ export const useSysStore = defineStore('sys', () => {
'1': '对账失败',
'2': '对账成功',
},
source: {
'0': '一路健康',
'1': '吴门医述',
},
orderType: {
'0': '充值',
'1': 'vip',
'2': '课',
'3': '实物',
'4': '培训班',
},
} as const;
// 自动推断字典类型
@@ -64,6 +75,38 @@ export const useSysStore = defineStore('sys', () => {
color: 'success',
},
],
source: [
{
label: '一路健康',
value: '0',
},
{
label: '吴门医述',
value: '1',
},
],
orderType: [
{
label: '充值',
value: '0',
},
{
label: 'vip',
value: '1',
},
{
label: '课',
value: '2',
},
{
label: '实物',
value: '3',
},
{
label: '培训班',
value: '4',
},
],
} as const;
// 自动推断字典列表类型

View File

@@ -62,7 +62,6 @@ const formOptions: VbenFormProps = {
defaultValue: '',
fieldName: 'checkoff',
label: '核对状态',
// 核对状态占1列
formItemClass: 'col-span-2',
},
],

View File

@@ -326,7 +326,6 @@ function onCompleteCheckCreated() {
:deep(.ant-card-body) {
padding: 1px !important;
height: calc(100% - 41px); // 减去标签页头部高度
overflow: hidden;
// background-color: #f1f3f6;
}
}

View File

@@ -0,0 +1,214 @@
<script lang="ts" setup>
import type { VbenFormProps } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { Button, message, notification } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { tianyibiApi } from '#/api/posting';
import { useSysStore } from '#/store';
const sysStore = useSysStore();
interface RowType {
id: number;
source: number;
orderSn: string;
orderOldId: string;
tel: string;
fee: number;
type: number;
orderTime: string;
useFlag: string;
}
const formOptions: VbenFormProps = {
// 默认展开
collapsed: false,
wrapperClass: 'grid-cols-6',
schema: [
{
component: 'RadioGroup',
componentProps: {
options: [
{
label: '未使用',
value: '0',
},
{
label: '已使用',
value: '1',
},
],
optionType: 'button',
},
defaultValue: '0',
fieldName: 'useFlag',
label: '状态',
},
{
component: 'Input',
componentProps: {
allowClear: true,
},
fieldName: 'tel',
label: '手机号',
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: sysStore.getDictList('source'),
},
fieldName: 'source',
label: '来源',
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: sysStore.getDictList('orderType').filter((item) => item.value !== '0'),
},
fieldName: 'type',
label: '订单类型',
},
{
component: 'RangePicker',
componentProps: {
allowClear: true,
placeholder: ['开始时间', '结束时间'],
valueFormat: 'YYYY-MM-DD',
},
fieldName: 'orderTime',
label: '订单时间',
formItemClass: 'col-span-2',
},
],
// 控制表单是否显示折叠按钮
showCollapseButton: false,
// 是否在字段值改变时提交表单
submitOnChange: true,
// 按下回车时是否提交表单
submitOnEnter: false,
};
// 处理订单时间范围
const handleFormValues = (values: any) => {
const { orderTime, ...rest } = values;
return orderTime && orderTime.length === 2
? {
...rest,
startTime: orderTime[0],
endTime: orderTime[1],
}
: {
...rest,
startTime: '',
endTime: '',
};
};
const gridOptions: VxeTableGridOptions<RowType> = {
columns: [
{ field: 'orderSn', title: '订单编号', width: 250 },
{
field: 'source',
title: '来源',
formatter: ({ cellValue }) => {
return sysStore.getDictMap('source', cellValue);
},
},
{ field: 'tel', title: '手机号' },
{ field: 'fee', title: '金额' },
{
field: 'type',
title: '类型',
formatter: ({ cellValue }) => {
return sysStore.getDictMap('orderType', cellValue);
},
},
{ field: 'orderTime', title: '订单时间', formatter: 'formatDateTime', minWidth: 160 },
{
title: '操作',
slots: {
default: 'action',
},
},
],
exportConfig: {},
height: 'auto',
keepSource: true,
rowConfig: {
isHover: true,
},
pagerConfig: {
enabled: true,
},
proxyConfig: {
response: {
result: 'records',
list: 'records',
total: 'total',
},
ajax: {
query: async ({ page }, formValues) => {
const params = handleFormValues(formValues);
return await tianyibiApi.getPointOrdersList({
page: page.currentPage,
limit: page.pageSize,
...params,
});
},
},
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
async function onStartAutoMatch() {
const hide = message.loading('系统自动匹配消耗中...', 0);
await tianyibiApi.autoConsumeTianyibi();
hide();
notification.success({
message: '自动匹配消耗成功',
});
// 刷新表格数据
gridApi?.query();
}
async function onConsumption(row: RowType, code: number) {
const api = code === 1 ? tianyibiApi.manualConsumeTianyibi : tianyibiApi.cancelConsumeTianyibi;
await api({ orderId: row.id });
notification.success({
message: code === 1 ? '确认消耗成功' : '取消消耗成功',
});
// 刷新表格数据
gridApi?.query();
}
</script>
<template>
<Page auto-content-height>
<Grid>
<template #toolbar-actions>
<div class="flex gap-2">
<Button type="primary" @click="onStartAutoMatch()">启动自动匹配消耗</Button>
</div>
</template>
<template #action="{ row }">
<Button v-if="!row.useFlag" type="primary" size="small" @click="onConsumption(row, 1)">
确认消耗
</Button>
<Button v-else type="primary" danger ghost size="small" @click="onConsumption(row, 0)">
取消消耗
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { VbenFormProps } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { balanceApi } from '#/api/posting';
interface RowType {
id: number;
tel: string;
totalPoint: number;
point: number;
}
const formOptions: VbenFormProps = {
// 默认展开
collapsed: false,
wrapperClass: 'grid-cols-6',
schema: [
{
component: 'Input',
componentProps: {
allowClear: true,
},
fieldName: 'tel',
label: '手机号',
},
],
// 控制表单是否显示折叠按钮
showCollapseButton: false,
// 是否在字段值改变时提交表单
submitOnChange: true,
// 按下回车时是否提交表单
submitOnEnter: false,
};
const gridOptions: VxeTableGridOptions<RowType> = {
columns: [
{ field: 'tel', title: '用户手机号' },
{ field: 'totalPoint', title: '总充值' },
{ field: 'point', title: '可用' },
],
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 balanceApi.getUserBalanceList({
page: page.currentPage,
limit: page.pageSize,
...formValues,
});
},
},
},
};
const [Grid] = useVbenVxeGrid({
formOptions,
gridOptions,
});
</script>
<template>
<Page auto-content-height>
<Grid />
</Page>
</template>