feat(mobile-oa-h5): implement P0 money/upload/receiveAccount/relatedApproval fields

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-11 12:17:33 +08:00
1 parent f0e687eec9
commit bb86eb7ad2
12 files changed
+910 -8

No files matched your search

@@ -11,7 +11,14 @@
- [ ] 基础字段可填、自研 stub 可见、校验按钮可用
- 问题列表:
1. (验收时填写)
- 下一步:开始 P0 真实控件(`upload` / `money` / `relatedApproval` / `receiveAccount`)移植计划
- 下一步:P1(`dept`/`person`)与套件;正式 `/oa-start` 联调
## P0 真实组件(已落地)
- `money`:数字输入 + 人民币大写
- `upload`:`van-uploader` → `/web/admin/attachment/upload`(需 URL `token`)
- `receiveAccount`:底部多选弹层 + options API
- `relatedApproval`:关联列表 + 候选弹层(mine/done/cc)
## 本地联调提示
+29
View File
@@ -0,0 +1,29 @@
import { get, post, request } from '../utils/http.js'
export function fetchReceiveAccountOptions() {
return get('/web/admin/oa-approval/receive-accounts/options')
}
export function fetchRelatedCandidates(params) {
return get('/web/admin/oa-approval/related-candidates', params)
}
export function fetchRelatedSummaries(ids, mode = 'view') {
return get('/web/admin/oa-approval/related-summaries', {
ids: (ids || []).filter(Boolean).join(','),
mode
})
}
export function uploadAttachment(file) {
const fd = new FormData()
fd.append('file', file)
fd.append('folder', 'oa-approval')
return request({
url: '/web/admin/attachment/upload',
method: 'POST',
data: fd
})
}
export { post }
+3
View File
@@ -7,6 +7,9 @@ import router from './router'
import './styles/tokens.css'
import { registerOaComponents } from './oa/register-components'
// 三墙主色(勿用 web-admin #10b981)
document.documentElement.style.setProperty('--van-primary-color', '#0a9f6d')
const app = createApp(App)
app.use(router)
app.use(Vant)
@@ -0,0 +1,70 @@
<template>
<div class="oa-money">
<van-field
v-model="text"
type="number"
inputmode="decimal"
:disabled="disabled"
:placeholder="placeholder"
@update:model-value="onInput"
/>
<div v-if="showUpper && upperText" class="oa-money__upper">{{ upperText }}</div>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { toRmbUpper } from '../../utils/rmb-upper.js'
defineOptions({ name: 'money' })
const props = defineProps({
modelValue: { type: [Number, String], default: null },
disabled: { type: Boolean, default: false },
showUpper: { type: Boolean, default: true },
placeholder: { type: String, default: '请输入金额' },
formCreateInject: { type: Object, default: null }
})
const emit = defineEmits(['update:modelValue', 'change'])
function toNum(v) {
if (v == null || v === '') return null
const n = typeof v === 'number' ? v : Number(String(v).replace(/,/g, ''))
if (!Number.isFinite(n) || n < 0) return null
return Math.round(n * 100) / 100
}
const text = ref(props.modelValue == null || props.modelValue === '' ? '' : String(props.modelValue))
const numeric = computed(() => toNum(props.modelValue))
const upperText = computed(() => {
const n = numeric.value
return n == null ? '' : toRmbUpper(n)
})
watch(
() => props.modelValue,
(v) => {
const next = v == null || v === '' ? '' : String(v)
if (next !== text.value) text.value = next
}
)
function onInput(v) {
const n = toNum(v)
emit('update:modelValue', n)
emit('change', n)
}
</script>
<style scoped>
.oa-money {
width: 100%;
}
.oa-money__upper {
margin: 4px 12px 0;
color: var(--text-muted, #6b7b74);
font-size: 12px;
line-height: 1.4;
word-break: break-word;
}
</style>
@@ -0,0 +1,174 @@
<template>
<div class="oa-recv">
<van-field
:model-value="summary"
readonly
is-link
:disabled="disabled"
placeholder="请选择收款账户"
@click="openPicker"
/>
<div v-if="normalized.length" class="oa-recv__detail">
<div v-for="item in normalized" :key="item.id" class="oa-recv__line">
{{ formatReceiveAccountLine(item) }}
</div>
</div>
<van-popup v-model:show="visible" position="bottom" round :style="{ height: '70%' }">
<div class="oa-recv__sheet">
<div class="oa-recv__hd">
<span @click="visible = false">取消</span>
<strong>选择收款账户</strong>
<span class="oa-recv__ok" @click="confirm">确定</span>
</div>
<van-search v-model="keyword" placeholder="搜索账户名称或账号" />
<div class="oa-recv__list">
<van-checkbox-group v-model="pendingIds">
<van-cell
v-for="a in filtered"
:key="a.id"
clickable
:title="a.label"
@click="toggle(a.id)"
>
<template #right-icon>
<van-checkbox :name="a.id" @click.stop />
</template>
</van-cell>
</van-checkbox-group>
<van-empty v-if="!loading && !filtered.length" description="暂无收款账户" />
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { showToast } from 'vant'
import { fetchReceiveAccountOptions } from '../../api/oa.js'
import {
formatReceiveAccountLine,
normalizeReceiveAccountModelValue,
optionLabelFromRow,
snapshotFromRow
} from '../../utils/receive-account-value.js'
defineOptions({ name: 'receiveAccount' })
const props = defineProps({
modelValue: { type: [Array, Object], default: () => [] },
disabled: { type: Boolean, default: false },
formCreateInject: { type: Object, default: null }
})
const emit = defineEmits(['update:modelValue', 'change'])
const visible = ref(false)
const loading = ref(false)
const keyword = ref('')
const pendingIds = ref([])
const snapshotMap = ref(new Map())
const accountOptions = ref([])
const normalized = computed(() => normalizeReceiveAccountModelValue(props.modelValue))
const summary = computed(() => {
const n = normalized.value.length
if (!n) return ''
if (n === 1) return formatReceiveAccountLine(normalized.value[0])
return `已选 ${n} 个账户`
})
const filtered = computed(() => {
const kw = keyword.value.trim().toLowerCase()
if (!kw) return accountOptions.value
return accountOptions.value.filter((a) => a.label.toLowerCase().includes(kw))
})
function emitItems(items) {
emit('update:modelValue', items)
emit('change', items)
}
function openPicker() {
if (props.disabled) return
pendingIds.value = normalized.value.map((x) => x.id)
visible.value = true
if (!accountOptions.value.length) loadOptions()
}
function toggle(id) {
const set = new Set(pendingIds.value)
if (set.has(id)) set.delete(id)
else set.add(id)
pendingIds.value = [...set]
}
function confirm() {
const items = pendingIds.value
.map((id) => snapshotMap.value.get(id))
.filter(Boolean)
emitItems(items)
visible.value = false
}
async function loadOptions() {
loading.value = true
try {
const res = await fetchReceiveAccountOptions()
const list = res?.list || []
const map = new Map()
const opts = []
for (const row of list) {
const snap = snapshotFromRow(row)
if (!snap.id) continue
map.set(snap.id, snap)
opts.push({ id: snap.id, label: optionLabelFromRow(snap) })
}
for (const x of normalized.value) {
if (!map.has(x.id)) map.set(x.id, x)
}
snapshotMap.value = map
accountOptions.value = opts
} catch (e) {
showToast(e?.message || '加载收款账户失败')
} finally {
loading.value = false
}
}
onMounted(() => {
loadOptions()
})
</script>
<style scoped>
.oa-recv__detail {
padding: 0 12px 8px;
color: var(--text-muted, #6b7b74);
font-size: 12px;
line-height: 1.5;
}
.oa-recv__line + .oa-recv__line {
margin-top: 4px;
}
.oa-recv__sheet {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
}
.oa-recv__hd {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid var(--border, #dcebe4);
}
.oa-recv__ok {
color: var(--brand-green, #0a9f6d);
font-weight: 600;
}
.oa-recv__list {
flex: 1;
overflow: auto;
}
</style>
@@ -0,0 +1,290 @@
<template>
<div class="oa-rel">
<div v-if="!ids.length" class="oa-rel__empty">
<span>暂未关联审批单</span>
<van-button v-if="!disabled" size="small" type="primary" plain @click="openPicker">
添加关联
</van-button>
</div>
<div v-else class="oa-rel__list">
<div v-for="item in displayItems" :key="item.id" class="oa-rel__card">
<div class="oa-rel__head">
<div class="oa-rel__title">
{{ item.unavailable ? '无法查看' : item.appName || '审批单' }}
<template v-if="!item.unavailable && item.applyTime">
· {{ item.applyTime }}
</template>
</div>
<van-button
v-if="!disabled"
size="mini"
type="danger"
plain
@click="removeId(item.id)"
>
删除
</van-button>
</div>
<div
v-for="(f, idx) in (item.fields || []).slice(0, 3)"
:key="idx"
class="oa-rel__field"
>
<span class="oa-rel__label">{{ f.label }}</span>
<span>{{ f.value }}</span>
</div>
</div>
<van-button
v-if="!disabled"
block
size="small"
type="primary"
plain
@click="openPicker"
>
添加关联
</van-button>
</div>
<van-popup v-model:show="visible" position="bottom" round :style="{ height: '80%' }">
<div class="oa-rel__sheet">
<div class="oa-rel__hd">
<span @click="visible = false">取消</span>
<strong>选择关联审批单</strong>
<span class="oa-rel__ok" @click="confirm">确定</span>
</div>
<van-tabs v-model:active="activeTab" @change="onTabChange">
<van-tab title="我发起的" name="mine" />
<van-tab title="我审批的" name="done" />
<van-tab title="抄送我的" name="cc" />
</van-tabs>
<van-search
v-model="keyword"
placeholder="搜索流程名 / 标题"
@search="loadCandidates"
/>
<div class="oa-rel__picker">
<van-checkbox-group v-model="pendingIds">
<van-cell
v-for="row in candidates"
:key="row.id"
clickable
:title="row.displayTitle || row.appName || '审批单'"
:label="pickerLabel(row)"
@click="toggle(row.id)"
>
<template #right-icon>
<van-checkbox :name="row.id" @click.stop />
</template>
</van-cell>
</van-checkbox-group>
<van-empty
v-if="!pickerLoading && !candidates.length"
description="暂无审批单"
/>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { showToast } from 'vant'
import { fetchRelatedCandidates, fetchRelatedSummaries } from '../../api/oa.js'
defineOptions({ name: 'relatedApproval' })
const props = defineProps({
modelValue: { type: [Array, String], default: () => [] },
disabled: { type: Boolean, default: false },
formCreateInject: { type: Object, default: null }
})
const emit = defineEmits(['update:modelValue', 'change'])
const visible = ref(false)
const pickerLoading = ref(false)
const keyword = ref('')
const activeTab = ref('mine')
const candidates = ref([])
const pendingIds = ref([])
const summaries = ref([])
const ids = computed(() => {
const raw = props.modelValue
if (Array.isArray(raw)) return raw.map((x) => String(x || '').trim()).filter(Boolean)
if (raw == null || raw === '') return []
return [String(raw).trim()].filter(Boolean)
})
const displayItems = computed(() => {
const map = new Map(summaries.value.map((s) => [s.id, s]))
return ids.value.map((id) => {
const s = map.get(id)
if (s) return s
return {
id,
unavailable: false,
appName: '加载中…',
applyTime: '',
fields: []
}
})
})
function emitValue(next) {
const uniq = []
const seen = new Set()
for (const id of next) {
const t = String(id || '').trim()
if (!t || seen.has(t)) continue
seen.add(t)
uniq.push(t)
}
emit('update:modelValue', uniq)
emit('change', uniq)
}
function removeId(id) {
emitValue(ids.value.filter((x) => x !== id))
}
function openPicker() {
if (props.disabled) return
pendingIds.value = []
visible.value = true
loadCandidates()
}
function toggle(id) {
const set = new Set(pendingIds.value)
if (set.has(id)) set.delete(id)
else set.add(id)
pendingIds.value = [...set]
}
function confirm() {
emitValue([...ids.value, ...pendingIds.value])
visible.value = false
}
function onTabChange() {
loadCandidates()
}
function pickerLabel(row) {
const bits = []
if (row.initiatorUserName) bits.push(row.initiatorUserName)
if (row.statusText || row.status) bits.push(row.statusText || row.status)
if (row.applyTime) bits.push(row.applyTime)
return bits.join(' · ')
}
async function loadCandidates() {
pickerLoading.value = true
try {
const res = await fetchRelatedCandidates({
tab: activeTab.value,
keyword: keyword.value.trim() || undefined,
page: 1,
pageSize: 20
})
candidates.value = res?.list || []
} catch (e) {
candidates.value = []
showToast(e?.message || '加载失败')
} finally {
pickerLoading.value = false
}
}
async function loadSummaries() {
const list = ids.value
if (!list.length) {
summaries.value = []
return
}
try {
const res = await fetchRelatedSummaries(list, 'view')
summaries.value = res?.list || []
} catch {
summaries.value = []
}
}
watch(
() => ids.value.join(','),
() => {
loadSummaries()
},
{ immediate: true }
)
</script>
<style scoped>
.oa-rel {
padding: 4px 12px 8px;
}
.oa-rel__empty {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
padding: 10px 0;
color: var(--text-muted, #6b7b74);
font-size: 13px;
}
.oa-rel__card {
margin-bottom: 10px;
padding: 10px 12px;
background: #fff;
border: 1px solid var(--border, #dcebe4);
border-radius: 10px;
}
.oa-rel__head {
display: flex;
gap: 8px;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 6px;
}
.oa-rel__title {
flex: 1;
font-size: 14px;
font-weight: 600;
color: var(--text-primary, #1f2a26);
}
.oa-rel__field {
display: flex;
gap: 8px;
margin-top: 4px;
color: var(--text-muted, #6b7b74);
font-size: 12px;
}
.oa-rel__label {
flex: 0 0 auto;
color: #94a3b8;
}
.oa-rel__sheet {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
}
.oa-rel__hd {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid var(--border, #dcebe4);
}
.oa-rel__ok {
color: var(--brand-green, #0a9f6d);
font-weight: 600;
}
.oa-rel__picker {
flex: 1;
overflow: auto;
}
</style>
@@ -0,0 +1,121 @@
<template>
<div class="oa-upload">
<van-uploader
v-model="fileList"
:disabled="disabled"
:max-count="limit"
:after-read="onAfterRead"
:before-delete="onBeforeDelete"
multiple
/>
<div v-if="!getToken()" class="oa-upload__tip">未携带登录 token,上传可能失败</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { showToast } from 'vant'
import { uploadAttachment } from '../../api/oa.js'
import { getToken } from '../../utils/auth.js'
defineOptions({ name: 'upload' })
const props = defineProps({
modelValue: { type: [Array, String, Object], default: () => [] },
disabled: { type: Boolean, default: false },
limit: { type: Number, default: 9 },
formCreateInject: { type: Object, default: null }
})
const emit = defineEmits(['update:modelValue', 'change'])
function toUrlList(raw) {
if (raw == null || raw === '') return []
if (Array.isArray(raw)) {
return raw
.map((x) => {
if (typeof x === 'string') return x
if (x && typeof x === 'object') return String(x.url || x.fileUrl || '')
return ''
})
.filter(Boolean)
}
if (typeof raw === 'string') return [raw]
if (typeof raw === 'object') {
const u = String(raw.url || raw.fileUrl || '')
return u ? [u] : []
}
return []
}
function urlsToFileList(urls) {
return urls.map((url, i) => ({
url,
name: url.split('/').pop() || `file-${i}`,
status: 'done',
isString: true
}))
}
const fileList = ref(urlsToFileList(toUrlList(props.modelValue)))
watch(
() => props.modelValue,
(v) => {
const urls = toUrlList(v)
const cur = fileList.value
.filter((f) => f.status === 'done' || f.isString)
.map((f) => f.url)
.filter(Boolean)
if (urls.join('|') !== cur.join('|')) {
fileList.value = urlsToFileList(urls)
}
}
)
function emitUrls() {
const urls = fileList.value
.filter((f) => f.status === 'done' || f.isString)
.map((f) => f.url)
.filter(Boolean)
const limit = Number(props.limit) || 9
const value = limit === 1 ? urls[0] || '' : urls
emit('update:modelValue', value)
emit('change', value)
}
async function onAfterRead(item) {
const items = Array.isArray(item) ? item : [item]
for (const f of items) {
f.status = 'uploading'
try {
const res = await uploadAttachment(f.file)
const rawUrl = String(res?.fileUrl || res?.data?.fileUrl || '').trim()
if (!rawUrl) throw new Error('未返回文件地址')
f.url = rawUrl
f.status = 'done'
f.isString = true
} catch (e) {
f.status = 'failed'
showToast(e?.message || '上传失败')
}
}
fileList.value = fileList.value.filter((f) => f.status !== 'failed')
emitUrls()
}
function onBeforeDelete() {
setTimeout(() => emitUrls(), 0)
return true
}
</script>
<style scoped>
.oa-upload {
padding: 4px 12px 8px;
}
.oa-upload__tip {
margin-top: 4px;
color: #cf8a19;
font-size: 12px;
}
</style>
+17 -7
View File
@@ -1,11 +1,18 @@
import OaStubField from './components/OaStubField.vue'
import OaMoneyField from './components/OaMoneyField.vue'
import OaUploadField from './components/OaUploadField.vue'
import OaReceiveAccountSelect from './components/OaReceiveAccountSelect.vue'
import OaRelatedApproval from './components/OaRelatedApproval.vue'
/** 与 web-admin formCreate.component 名称对齐;阶段一可用 stub */
const CUSTOM_TYPES = [
'upload',
'money',
'relatedApproval',
'receiveAccount',
/** P0 真实组件;其余仍用 stub,阶段二继续替换 */
const REAL_COMPONENTS = {
money: OaMoneyField,
upload: OaUploadField,
receiveAccount: OaReceiveAccountSelect,
relatedApproval: OaRelatedApproval
}
const STUB_TYPES = [
'dept',
'person',
'ownerPerson',
@@ -21,7 +28,10 @@ const CUSTOM_TYPES = [
export function registerOaComponents(formCreate) {
if (!formCreate || typeof formCreate.component !== 'function') return
for (const name of CUSTOM_TYPES) {
for (const [name, comp] of Object.entries(REAL_COMPONENTS)) {
formCreate.component(name, comp)
}
for (const name of STUB_TYPES) {
formCreate.component(name, OaStubField)
}
}
+9
View File
@@ -0,0 +1,9 @@
/** 从 URL ?token= 读取登录态(小程序 web-view 传入) */
export function getToken() {
try {
const q = new URLSearchParams(window.location.search)
return String(q.get('token') || '').trim()
} catch {
return ''
}
}
+50
View File
@@ -0,0 +1,50 @@
import { getToken } from './auth.js'
const API_BASE = (import.meta.env.VITE_API_BASE || '/api').replace(/\/$/, '')
function unwrap(body) {
if (body == null || typeof body !== 'object') {
throw new Error('网络异常,请稍后重试')
}
if (body.code === 200) return body.data
throw new Error((body.message && String(body.message).trim()) || '请求失败')
}
export async function request({ url, method = 'GET', data, headers = {} }) {
const path = url.startsWith('/') ? url : `/${url}`
const full = `${API_BASE}${path}`
const token = getToken()
const init = {
method: method.toUpperCase(),
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
...headers
}
}
if (data instanceof FormData) {
init.body = data
} else if (data != null && init.method !== 'GET') {
init.headers['Content-Type'] = 'application/json'
init.body = JSON.stringify(data)
}
let finalUrl = full
if (init.method === 'GET' && data && typeof data === 'object') {
const q = new URLSearchParams()
Object.entries(data).forEach(([k, v]) => {
if (v != null && v !== '') q.set(k, String(v))
})
const s = q.toString()
if (s) finalUrl += (finalUrl.includes('?') ? '&' : '?') + s
}
const res = await fetch(finalUrl, init)
const body = await res.json().catch(() => null)
return unwrap(body)
}
export function get(url, params) {
return request({ url, method: 'GET', data: params })
}
export function post(url, data) {
return request({ url, method: 'POST', data })
}
@@ -0,0 +1,78 @@
export const ACCOUNT_TYPE_LABEL = {
alipay: '支付宝',
personal: '个人银行卡',
corporate: '对公银行账户'
}
function isReceiveAccountType(v) {
return v === 'alipay' || v === 'personal' || v === 'corporate'
}
export function isReceiveAccountSnapshot(v) {
if (!v || typeof v !== 'object') return false
if (!isReceiveAccountType(v.accountType)) return false
return String(v.id || '').trim() !== ''
}
function trimSnapshot(v) {
return {
id: String(v.id).trim(),
accountType: v.accountType,
accountName: String(v.accountName || '').trim(),
accountNo: String(v.accountNo || '').trim(),
bankName: String(v.bankName || '').trim(),
bankProvince: String(v.bankProvince || '').trim(),
bankCity: String(v.bankCity || '').trim(),
bankBranch: String(v.bankBranch || '').trim(),
remark: String(v.remark || '').trim()
}
}
export function normalizeReceiveAccountModelValue(raw) {
if (Array.isArray(raw)) {
return raw.filter(isReceiveAccountSnapshot).map(trimSnapshot)
}
if (isReceiveAccountSnapshot(raw)) return [trimSnapshot(raw)]
return []
}
export function formatReceiveAccountLine(item) {
const parts = [
ACCOUNT_TYPE_LABEL[item.accountType],
item.accountName,
item.accountNo
]
if (item.accountType === 'personal' || item.accountType === 'corporate') {
if (item.bankName) parts.push(item.bankName)
if (item.bankProvince && item.bankCity) {
parts.push(`${item.bankProvince} / ${item.bankCity}`)
} else if (item.bankProvince || item.bankCity) {
parts.push(item.bankProvince || item.bankCity)
}
if (item.bankBranch) parts.push(item.bankBranch)
}
if (item.remark) parts.push(item.remark)
return parts.filter(Boolean).join(' · ')
}
export function optionLabelFromRow(row) {
const typeLabel = isReceiveAccountType(row.accountType)
? ACCOUNT_TYPE_LABEL[row.accountType]
: row.accountType
return [typeLabel, row.accountName, row.accountNo].filter(Boolean).join(' · ')
}
export function snapshotFromRow(row) {
const accountType = row?.accountType
return {
id: String(row?.id ?? '').trim(),
accountType: isReceiveAccountType(accountType) ? accountType : 'personal',
accountName: String(row?.accountName ?? '').trim(),
accountNo: String(row?.accountNo ?? '').trim(),
bankName: String(row?.bankName ?? '').trim(),
bankProvince: String(row?.bankProvince ?? '').trim(),
bankCity: String(row?.bankCity ?? '').trim(),
bankBranch: String(row?.bankBranch ?? '').trim(),
remark: String(row?.remark ?? '').trim()
}
}
+61
View File
@@ -0,0 +1,61 @@
/** 与 web-admin toRmbUpper 对齐的人民币大写 */
export function toRmbUpper(value) {
if (value == null || !Number.isFinite(value) || value < 0) return ''
if (value > 999999999999.99) return '金额过大'
const fen = Math.round(value * 100)
if (fen === 0) return '零元整'
const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
const units = ['', '拾', '佰', '仟']
function sectionToChinese(n) {
if (n === 0) return ''
let result = ''
let zeroPending = false
for (let i = 0; i < 4; i++) {
const digit = Math.floor(n / 10 ** (3 - i)) % 10
if (digit === 0) {
if (result !== '') zeroPending = true
} else {
if (zeroPending) {
result += '零'
zeroPending = false
}
result += digits[digit] + units[3 - i]
}
}
return result
}
const yuan = Math.floor(fen / 100)
const jiao = Math.floor((fen % 100) / 10)
const fenDigit = fen % 10
let result = ''
if (yuan > 0) {
const yi = Math.floor(yuan / 100000000)
const wan = Math.floor(yuan / 10000) % 10000
const rest = yuan % 10000
if (yi > 0) result += sectionToChinese(yi) + '亿'
if (wan > 0) {
if (yi > 0 && wan < 1000) result += '零'
result += sectionToChinese(wan) + '万'
}
if (rest > 0) {
if (wan > 0) {
if (rest < 1000) result += '零'
} else if (yi > 0) {
result += '零'
}
result += sectionToChinese(rest)
}
result += '元'
}
if (jiao > 0) result += digits[jiao] + '角'
else if (fenDigit > 0 && yuan > 0) result += '零'
if (fenDigit > 0) result += digits[fenDigit] + '分'
if (jiao === 0 && fenDigit === 0) result += '整'
return result
}