Compare commits

...

2 Commits

Author SHA1 Message Date
Ironsp
6af8fd7b09 feat 财务中心 2026-10-09 17:17:56 +08:00
Ironsp
123d259f92 feat 合同中心 2026-10-09 17:17:43 +08:00
18 changed files with 812 additions and 627 deletions

View File

@@ -34,6 +34,8 @@ defineProps<Props>()
}
&__slogan {
// 设计稿 #20:slogan 水平居中(当前唯一使用方为财务中心页)
text-align: center;
padding-bottom: 0;
margin-top: 9px;
font-size: 13px;

View File

@@ -275,3 +275,14 @@
@finance-entry-text: #4b5563;
@finance-axis-text: #9ca3af;
@finance-ink: #1f2937;
// 财务中心首页快捷入口图标(设计稿 #20,蓝湖 fallback 无切图,色值取自封面采样)
@finance-entry-bill-bg: #e9f1fe;
@finance-entry-bill-icon: #1f67ef;
@finance-entry-pending-bg: #fff4e5;
@finance-entry-invoice-bg: #f1ecfe;
@finance-entry-invoice-icon: #7c5ce0;
@finance-entry-export-bg: #e6f6ec;
@finance-entry-badge-bg: #f53f3f;
@finance-chart-switch-active: #024fff;
@finance-switch-active-bg: #e8f0ff;
@finance-switch-idle-bg: #f2f4f7;

View File

@@ -118,9 +118,9 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
&__section {
position: absolute;
left: 43px;
left: 54px;
top: 0;
height: 0;
height: 70px;
&--1 { top: 36px; }
&--2 { top: 649px; }
@@ -130,11 +130,13 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
&__section-title {
position: absolute;
left: 60px;
top: 23px;
margin: 0;
font-size: 28px;
font-weight: 500;
line-height: 41px;
color: @contract-cell-text;
white-space: nowrap; // 父级 h3 内子元素全部脱流,标题会逐字换行,强制不换行
}
&__step {
@@ -164,8 +166,8 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
&__field--5:has(&__label) { top: 749px; }
&__field--l { left: 64px; }
&__field--r { left: 940px; }
&__field--l { left: 54px; }
&__field--r { left: 930px; }
&__label {
font-size: 20px;

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { ElTable, ElTableColumn } from 'element-plus'
import { StatusTag } from '@/components'
import type { Contract, SignStatus } from '@/types/business/contract'
import { formatMoney } from '@/utils/format'
@@ -23,125 +24,96 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
<template>
<div class="contract-table">
<table class="contract-table__grid">
<thead>
<tr>
<th>合同编号</th>
<th>合同名称</th>
<th>合同类型</th>
<th>合同金额</th>
<th>关联项目</th>
<th>项目编号</th>
<th>签署状态</th>
<th>生效时间</th>
<th>到期时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in contracts" :key="item.id">
<td>{{ item.contractNo }}</td>
<td class="contract-table__name" :title="item.contractName">
{{ item.contractName }}
</td>
<td>{{ item.contractType }}</td>
<td>¥{{ formatMoney(item.contractAmount) }}</td>
<td class="contract-table__name" :title="item.relatedProjectName">
{{ item.relatedProjectName }}
</td>
<td>{{ item.projectNo }}</td>
<td>
<StatusTag :label="item.signStatus" :variant="SIGN_VARIANTS[item.signStatus]" />
</td>
<td>{{ item.effectiveDate }}</td>
<td>{{ item.expiryDate }}</td>
<td class="contract-table__actions">
<el-table
:data="contracts"
border
style="width: 100%"
>
<el-table-column prop="contractNo" label="合同编号" show-overflow-tooltip />
<el-table-column prop="contractName" label="合同名称" show-overflow-tooltip />
<el-table-column prop="contractType" label="合同类型" show-overflow-tooltip />
<el-table-column label="合同金额" show-overflow-tooltip>
<template #default="{ row }">
¥{{ formatMoney((row as Contract).contractAmount) }}
</template>
</el-table-column>
<el-table-column prop="relatedProjectName" label="关联项目" show-overflow-tooltip />
<el-table-column prop="projectNo" label="项目编号" show-overflow-tooltip />
<el-table-column label="签署状态" width="140">
<template #default="{ row }">
<StatusTag :label="(row as Contract).signStatus" :variant="SIGN_VARIANTS[(row as Contract).signStatus]" />
</template>
</el-table-column>
<el-table-column prop="effectiveDate" label="生效时间" show-overflow-tooltip />
<el-table-column prop="expiryDate" label="到期时间" show-overflow-tooltip />
<el-table-column label="操作" width="180">
<template #default="{ row }">
<div class="contract-table__actions">
<button
v-if="item.signStatus === '待签署'"
v-if="(row as Contract).signStatus === '待签署'"
type="button"
class="contract-table__btn contract-table__btn--primary"
@click="emit('sign', item.id)"
@click="emit('sign', (row as Contract).id)"
>
去签署
</button>
<button class="contract-table__btn" @click="emit('view', item.id)">
<button
type="button"
class="contract-table__btn"
@click="emit('view', (row as Contract).id)"
>
详情
</button>
</td>
</tr>
<tr v-if="contracts.length === 0">
<td colspan="10" class="contract-table__empty text-secondary">
暂无符合条件的合同
</td>
</tr>
</tbody>
</table>
</div>
</template>
</el-table-column>
<template #empty>
<span class="contract-table__empty">暂无符合条件的合同</span>
</template>
</el-table>
</div>
</template>
<style lang="less" scoped>
.contract-table {
overflow-x: auto;
.scrollbar();
&__grid {
width: 100%;
border: 1px solid @contract-line;
border-collapse: collapse;
table-layout: fixed;
:deep(.el-table) {
font-size: 20px;
--el-table-border-color: @contract-line;
--el-table-header-bg-color: @contract-table-head-bg;
--el-table-header-text-color: @contract-head-text;
--el-table-text-color: @contract-cell-text;
--el-table-row-hover-bg-color: @bg-color-light;
background: @white;
th,
td {
.el-table__cell {
padding: 0;
}
.cell {
padding: 0 10px;
border-bottom: 1px solid @contract-line;
vertical-align: middle;
white-space: nowrap;
font-size: 20px;
}
td.contract-table__name {
white-space: normal;
word-break: break-all;
}
th {
height: 64px;
padding-top: 0;
padding-bottom: 0;
background: @contract-table-head-bg;
color: @contract-head-text;
font-weight: 400;
text-align: center;
}
td {
height: 96px;
color: @contract-cell-text;
line-height: 29px;
}
tbody tr:last-child td {
border-bottom: none;
th.el-table__cell {
height: 64px;
font-weight: 400;
background: @contract-table-head-bg;
.cell {
color: @contract-head-text;
text-align: center;
}
}
tbody tr:hover td {
td.el-table__cell {
height: 96px;
color: @contract-cell-text;
}
.el-table__row:hover > td.el-table__cell {
background: @bg-color-light;
}
th:nth-child(1) { width: 11%; }
th:nth-child(2) { width: 9%; }
th:nth-child(3) { width: 10%; }
th:nth-child(4) { width: 9%; }
th:nth-child(5) { width: 10%; }
th:nth-child(6) { width: 11%; }
th:nth-child(7) { width: 7%; }
th:nth-child(8) { width: 11%; }
th:nth-child(9) { width: 11%; }
th:nth-child(10) { width: 11%; }
}
&__name {
color: @contract-cell-text;
}
&__actions {
@@ -152,6 +124,9 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
&__btn {
font-size: 13px;
color: @contract-accent;
background: transparent;
border: none;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
@@ -161,7 +136,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
&--primary {
height: 28px;
padding: 0 @spacing-sm;
border: none;
border-radius: @radius-sm;
background: @contract-accent;
color: @white;
@@ -173,6 +147,12 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
}
}
&__empty {
display: block;
padding: @spacing-xl 0;
color: @text-color-secondary;
}
:deep(.status-tag) {
padding: 0;
border-radius: 0;
@@ -180,10 +160,5 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
color: inherit;
font-size: inherit;
}
&__empty {
padding: @spacing-xl 0;
text-align: center;
}
}
</style>

View File

@@ -1,15 +1,13 @@
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useRoute, useRouter } from 'vue-router'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { AppButton } from '@/components'
import type { FileInfo } from '@/types/api'
import { useContractStore } from '@/stores/modules/contract'
import ContractInfoCard from './cpns/ContractInfoCard.vue'
const route = useRoute()
const router = useRouter()
const contractStore = useContractStore()
const { current } = storeToRefs(contractStore)
@@ -19,10 +17,6 @@ function load(): void {
void contractStore.fetchDetail(contractId.value)
}
function onBack(): void {
void router.push('/contract')
}
function onViewProject(): void {
// TODO: 关联项目 id 由接口返回后跳转项目详情
ElMessage.info('项目详情跳转待接口接入')
@@ -65,12 +59,6 @@ onMounted(load)
<p v-else class="contract-detail__empty text-secondary">
合同不存在或已下线
</p>
<div class="contract-detail__footer">
<AppButton @click="onBack">
返回
</AppButton>
</div>
</div>
</div>
</template>
@@ -91,28 +79,6 @@ onMounted(load)
color: @contract-muted;
}
&__footer {
.flex(row, flex-end);
width: 100%;
margin-top: 30px;
:deep(.app-button) {
height: 40px;
padding: 0 32px;
border: none;
border-radius: @radius-base;
background: @primary-strong;
color: @white;
font-size: 16px;
}
:deep(.app-button:hover) {
border-color: transparent;
background: @primary-color;
color: @white;
}
}
&__empty {
padding: @spacing-xxl 0;
text-align: center;

View File

@@ -2,6 +2,7 @@
import { computed, onMounted, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useRoute, useRouter } from 'vue-router'
import { ElButton } from 'element-plus'
import { useFinanceStore } from '@/stores/modules/finance'
import type { Bill, BillStatus } from '@/types/business/finance'
import { formatMoney } from '@/utils/format'
@@ -122,20 +123,20 @@ onMounted(() => {
</div>
</div>
<button
<el-button
v-if="card.status === '待支付'"
class="bill-detail__cta"
@click="onPay(currentBill.id)"
>
立即支付
</button>
<button
</el-button>
<el-button
v-else
class="bill-detail__cta"
@click="router.push('/finance/bills')"
>
返回
</button>
</el-button>
</section>
</div>
@@ -350,12 +351,12 @@ onMounted(() => {
width: 90px;
height: 32px;
border: none;
padding: 0;
border-radius: 4px;
background: @primary-strong;
color: @white;
font-size: 12px;
font-weight: 500;
padding-bottom: 6px;
box-sizing: border-box;
transform: translateX(-50%);
transition: background 0.2s;

View File

@@ -3,6 +3,7 @@ import { computed, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'
import { PaginationBar } from '@/components'
import { ElRadioGroup, ElRadioButton, ElInput, ElButton } from 'element-plus'
import { useFinanceStore } from '@/stores/modules/finance'
import type { BillStatus } from '@/types/business/finance'
import BillSummaryBar from './cpns/BillSummaryBar.vue'
@@ -35,6 +36,10 @@ function onTab(value: BillStatus | 'all'): void {
void financeStore.search()
}
function onTabChange(val: string | number | boolean | undefined): void {
onTab(val as BillStatus | 'all')
}
function onSearch(): void {
financeStore.setQuery({ ...query.value, keyword: keyword.value })
void financeStore.search()
@@ -87,31 +92,34 @@ onMounted(() => {
</div>
<div class="bills__toolbar">
<div class="bills__tabs">
<button
<el-radio-group
class="bills__tabs"
:model-value="query.status"
@change="onTabChange"
>
<el-radio-button
v-for="tab in STATUS_TABS"
:key="tab.value"
class="bills__tab"
:class="{ 'is-active': query.status === tab.value }"
@click="onTab(tab.value)"
:value="tab.value"
>
{{ tab.label }}
</button>
</div>
</el-radio-button>
</el-radio-group>
<input
<el-input
v-model="keyword"
class="bills__search"
placeholder="关键字搜索"
clearable
@keyup.enter="onSearch"
>
/>
<button class="bills__btn bills__btn--reset" @click="onReset">
<el-button class="bills__btn bills__btn--reset" @click="onReset">
重置
</button>
<button class="bills__btn bills__btn--primary" @click="onSearch">
</el-button>
<el-button class="bills__btn bills__btn--primary" @click="onSearch">
搜索
</button>
</el-button>
</div>
<div class="bills__board">
@@ -220,44 +228,32 @@ onMounted(() => {
}
&__tabs {
.flex(row, flex-start, center);
gap: 0;
width: 279px;
height: 34px;
border: 1px solid @border-color;
border-radius: 4px;
overflow: hidden;
flex-shrink: 0;
box-sizing: border-box;
padding: 0 11.3px 0 9.2px;
}
&__tab {
flex: 1 1 0;
height: 32px;
border: none;
border-right: 1px solid @border-color;
border-radius: 0;
background: @white;
color: @finance-entry-text;
font-size: 12px;
font-weight: 400;
transition: all 0.2s;
&:last-child {
border-right: none;
:deep(.el-radio-button__inner) {
height: 34px;
padding: 0 11.3px;
border: 1px solid @border-color;
border-radius: 4px;
background: @white;
color: @finance-entry-text;
font-size: 12px;
font-weight: 400;
line-height: 32px;
box-shadow: none;
}
&:hover {
color: @primary-strong;
:deep(.el-radio-button + .el-radio-button) {
margin-left: -1px;
}
&.is-active {
border-right-color: @primary-strong;
:deep(.el-radio-button.is-active .el-radio-button__inner) {
border-color: @primary-strong;
background: @primary-strong;
color: @white;
font-size: 11px;
font-weight: 500;
box-shadow: -1px 0 0 0 @primary-strong;
}
}
@@ -265,18 +261,24 @@ onMounted(() => {
width: 1300px;
height: 37px;
margin-left: 33px;
padding: 0 16px;
border: none;
border-radius: 0;
background: @finance-field-bg;
color: @finance-entry-text;
font-size: 13px;
outline: none;
box-sizing: border-box;
flex-shrink: 0;
&::placeholder {
color: @contract-placeholder;
:deep(.el-input__wrapper) {
width: 1300px;
height: 37px;
padding: 0 16px;
background: @finance-field-bg;
box-shadow: none;
border-radius: 0;
}
:deep(.el-input__inner) {
color: @finance-entry-text;
font-size: 13px;
&::placeholder {
color: @contract-placeholder;
}
}
}
@@ -284,7 +286,6 @@ onMounted(() => {
height: 32px;
border: none;
border-radius: 0;
background: transparent;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
@@ -293,9 +294,11 @@ onMounted(() => {
width: 62.2px;
margin-left: 15.4px;
color: @finance-entry-text;
background: transparent;
&:hover {
color: @primary-strong;
background: transparent;
}
}

View File

@@ -2,7 +2,7 @@
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { ElMessage, ElButton } from 'element-plus'
import { useFinanceStore } from '@/stores/modules/finance'
import type { PayMethod } from '@/types/business/finance'
import { createMerchantOrderNo, PAY_METHODS } from '@/utils/payMeta'
@@ -172,13 +172,13 @@ onBeforeUnmount(() => {
</div>
</div>
<button
<el-button
class="checkout__cta"
:disabled="!canPay"
@click="onConfirm"
>
{{ submitting ? '处理中…' : '确认支付' }}
</button>
</el-button>
<p class="checkout__summary-tip">
支付完成后订单状态将自动更新
@@ -191,9 +191,9 @@ onBeforeUnmount(() => {
<p class="checkout__empty-text">
未找到待支付账单,请返回账单明细重新选择
</p>
<button class="checkout__empty-btn" @click="goBack">
<el-button class="checkout__empty-btn" @click="goBack">
返回账单明细
</button>
</el-button>
</section>
<WechatQrDialog
@@ -283,6 +283,8 @@ onBeforeUnmount(() => {
&__cta {
width: 100%;
height: 44px;
border: none;
padding: 0;
border-radius: @radius-round;
background: @checkout-cta;
color: @white;
@@ -329,6 +331,12 @@ onBeforeUnmount(() => {
font-size: @font-size-base;
}
:deep(.checkout__empty-btn.el-button) {
border: 1px solid @checkout-cta;
background: @checkout-card;
color: @checkout-cta;
}
@media (max-width: 992px) {
&__layout {
grid-template-columns: minmax(0, 1fr);

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { ElCheckbox, ElButton } from 'element-plus'
import type { Bill } from '@/types/business/finance'
import { formatMoney } from '@/utils/format'
@@ -30,12 +31,11 @@ function onBatchPay(): void {
<div class="bill-summary">
<div class="bill-summary__inner">
<div class="bill-summary__left">
<input
<el-checkbox
class="bill-summary__check"
type="checkbox"
:checked="canPay"
:model-value="canPay"
@change="onBatchPay"
>
/>
<span class="bill-summary__text">已选择</span>
<span class="bill-summary__count">{{ selectedBills.length }}</span>
<span class="bill-summary__text">笔订单</span>
@@ -44,9 +44,9 @@ function onBatchPay(): void {
<div class="bill-summary__right">
<span class="bill-summary__text">合计:</span>
<span class="bill-summary__amount">¥ {{ formatMoney(selectedAmount, 0) }}</span>
<button class="bill-summary__cta" :disabled="!canPay" @click="onBatchPay">
<el-button class="bill-summary__cta" :disabled="!canPay" @click="onBatchPay">
立即支付
</button>
</el-button>
</div>
</div>
</div>
@@ -77,11 +77,15 @@ function onBatchPay(): void {
}
&__check {
width: 15px;
height: 15px;
margin: 0 12.25px 0 0;
accent-color: @primary-strong;
flex-shrink: 0;
:deep(.el-checkbox__inner) {
width: 15px;
height: 15px;
border-color: @border-color;
}
}
&__text {
@@ -118,6 +122,7 @@ function onBatchPay(): void {
width: 84px;
height: 40px;
border: none;
padding: 0;
border-radius: 4px;
background: @finance-summary-cta;
color: @white;

View File

@@ -1,5 +1,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { ref, watch } from 'vue'
import { ElTable, ElTableColumn } from 'element-plus'
import type { TableInstance } from 'element-plus'
import type { Bill } from '@/types/business/finance'
import { formatMoney } from '@/utils/format'
@@ -16,223 +18,158 @@ const emit = defineEmits<{
'update:selection': [ids: string[]]
}>()
const selection = ref<Set<string>>(new Set(props.selectedIds))
const tableRef = ref<TableInstance>()
function onSelectionChange(rows: Bill[]): void {
emit('update:selection', rows.map((row) => row.id))
}
// 外部清空选中(如重置查询)时同步清空 el-table 选择态
watch(
() => props.selectedIds,
(ids) => {
selection.value = new Set(ids)
if (ids.length === 0 && tableRef.value) tableRef.value.clearSelection()
},
)
const allSelected = computed(
() => props.bills.length > 0 && props.bills.every((bill) => selection.value.has(bill.id)),
)
const someSelected = computed(
() => props.bills.some((bill) => selection.value.has(bill.id)) && !allSelected.value,
)
function toggle(id: string): void {
if (selection.value.has(id)) selection.value.delete(id)
else selection.value.add(id)
emit('update:selection', [...selection.value])
}
function toggleAll(): void {
if (allSelected.value) {
for (const bill of props.bills) selection.value.delete(bill.id)
}
else {
for (const bill of props.bills) selection.value.add(bill.id)
}
emit('update:selection', [...selection.value])
}
const COLUMNS = [
{ key: 'index', label: '序号', className: 'bill-table__col--index' },
{ key: 'planName', label: '账单名称', className: 'bill-table__col--name' },
{ key: 'billNo', label: '账单号', className: 'bill-table__col--no' },
{ key: 'tradeNo', label: '交易流水号', className: 'bill-table__col--trade' },
{ key: 'projectName', label: '项目名称', className: 'bill-table__col--project' },
{ key: 'amount', label: '订单金额', className: 'bill-table__col--amount' },
{ key: 'status', label: '状态', className: 'bill-table__col--status' },
{ key: 'ops', label: '操作', className: 'bill-table__col--ops' },
] as const
</script>
<template>
<table class="bill-table">
<colgroup>
<col class="bill-table__col--check">
<col
v-for="column in COLUMNS"
:key="column.key"
:class="column.className"
>
</colgroup>
<thead>
<tr>
<th class="bill-table__cell bill-table__cell--check">
<input
type="checkbox"
:checked="allSelected"
:indeterminate="someSelected"
@change="toggleAll"
<el-table
ref="tableRef"
class="bill-table"
:data="bills"
row-key="id"
@selection-change="onSelectionChange"
>
<el-table-column
type="selection"
width="51"
/>
<el-table-column
type="index"
label="序号"
width="235"
/>
<el-table-column
prop="planName"
label="账单名称"
width="310"
show-overflow-tooltip
/>
<el-table-column
prop="billNo"
label="账单号"
width="148"
show-overflow-tooltip
/>
<el-table-column
prop="tradeNo"
label="交易流水号"
width="394"
show-overflow-tooltip
/>
<el-table-column
prop="projectName"
label="项目名称"
width="339"
show-overflow-tooltip
/>
<el-table-column
label="订单金额"
width="97"
>
<template #default="{ row }">
{{ formatMoney((row as Bill).amount, 0) }}
</template>
</el-table-column>
<el-table-column
label="状态"
width="95"
>
<template #default="{ row }">
{{ (row as Bill).status }}
</template>
</el-table-column>
<el-table-column
label="操作"
width="96"
fixed="right"
>
<template #default="{ row }">
<span class="bill-table__ops">
<el-button
v-if="(row as Bill).status === '待支付'"
link
type="primary"
class="bill-table__link"
@click="emit('pay', (row as Bill).id)"
>
</th>
<th
v-for="column in COLUMNS"
:key="column.key"
class="bill-table__cell"
>
{{ column.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(bill, index) in bills" :key="bill.id">
<td class="bill-table__cell bill-table__cell--check">
<input
type="checkbox"
:checked="selection.has(bill.id)"
@change="toggle(bill.id)"
去支付
</el-button>
<el-button
link
type="primary"
class="bill-table__link"
@click="emit('view', (row as Bill).id)"
>
</td>
<td class="bill-table__cell">
{{ index + 1 }}
</td>
<td class="bill-table__cell">
{{ bill.planName }}
</td>
<td class="bill-table__cell">
{{ bill.billNo }}
</td>
<td class="bill-table__cell">
{{ bill.tradeNo ?? '—' }}
</td>
<td class="bill-table__cell">
{{ bill.projectName }}
</td>
<td class="bill-table__cell">
{{ formatMoney(bill.amount, 0) }}
</td>
<td class="bill-table__cell">
{{ bill.status }}
</td>
<td class="bill-table__cell bill-table__cell--ops">
<span class="bill-table__ops">
<button
v-if="bill.status === '待支付'"
class="bill-table__link"
@click="emit('pay', bill.id)"
>
去支付
</button>
<button class="bill-table__link" @click="emit('view', bill.id)">
详情
</button>
</span>
</td>
</tr>
<tr v-if="bills.length === 0">
<td colspan="9" class="bill-table__empty">
暂无账单数据
</td>
</tr>
</tbody>
</table>
详情
</el-button>
</span>
</template>
</el-table-column>
<template #empty>
<span class="bill-table__empty">暂无账单数据</span>
</template>
</el-table>
</template>
<style lang="less" scoped>
.bill-table {
width: 1765px;
margin: 6.5px 0 0 14.5px;
border-collapse: collapse;
table-layout: fixed;
font-size: 12px;
color: @finance-entry-text;
&__col--check { width: 51px; }
&__col--index { width: 235.4px; }
&__col--name { width: 310.2px; }
&__col--no { width: 148px; }
&__col--trade { width: 394.1px; }
&__col--project { width: 339.4px; }
&__col--amount { width: 97px; }
&__col--status { width: 95px; }
&__col--ops { width: 95.5px; }
&__cell {
height: 43.77px;
padding: 0;
border-bottom: 1px solid @project-divider;
text-align: left;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
&:not(&--check):not(&--ops) {
padding-left: 10px;
}
&--check {
padding-left: 43.5px;
}
&--ops {
padding-right: 22px;
text-align: right;
}
}
@col-pad-1: 43.5px;
@col-pad-2: 149.9px;
@col-pad-3: 8px;
@col-pad-4: 17px;
@col-pad-5: 5px;
@col-pad-6: 14.6px;
@col-pad-7: 4.6px;
@col-pad-8: 6px;
thead &__cell,
tbody &__cell {
&:nth-child(1) { padding-left: @col-pad-1; }
&:nth-child(2) { padding-left: @col-pad-2; }
&:nth-child(3) { padding-left: @col-pad-3; }
&:nth-child(4) { padding-left: @col-pad-4; }
&:nth-child(5) { padding-left: @col-pad-5; }
&:nth-child(6) { padding-left: @col-pad-6; }
&:nth-child(7) { padding-left: @col-pad-7; }
&:nth-child(8) { padding-left: @col-pad-8; }
&:nth-child(9) { padding-left: 0; padding-right: 10px; }
}
thead &__cell {
:deep(.el-table__header-wrapper th.el-table__cell) {
height: 41.77px;
padding: 0;
background: @finance-table-head-bg;
border-right: 1px solid @project-divider;
border-bottom: 1px solid @project-divider;
color: @finance-entry-text;
font-size: 12px;
font-weight: 600;
&:first-child {
border-left: 1px solid @project-divider;
}
text-align: left;
}
tbody tr:hover {
:deep(.el-table__header-wrapper th.el-table__cell:first-child) {
border-left: 1px solid @project-divider;
}
:deep(.el-table__body-wrapper td.el-table__cell) {
height: 43.77px;
padding: 0;
border-bottom: 1px solid @project-divider;
box-sizing: border-box;
}
:deep(.el-table__body tr:hover > td.el-table__cell) {
background: @bg-color-light;
}
&__cell--check input {
width: 14px;
height: 14px;
margin: 0;
accent-color: @primary-strong;
vertical-align: middle;
:deep(.el-table .cell) {
padding-left: 10px;
padding-right: 10px;
line-height: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// 首列(多选)左缩进对齐设计稿
:deep(.el-table-column--selection .cell) {
padding-left: 43.5px;
}
&__ops {
@@ -240,31 +177,18 @@ const COLUMNS = [
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 18px;
white-space: nowrap;
}
&__link {
padding: 0;
border: none;
background: transparent;
color: @primary-strong;
font-size: 12px;
font-weight: 400;
transition: color 0.2s;
+ .bill-table__link {
margin-left: 18.36px;
}
&:hover {
color: @primary-color;
}
padding: 0;
}
&__empty {
height: 43.77px;
color: @project-muted;
text-align: center;
}
}
</style>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { ElButton } from 'element-plus'
interface DistributionItem {
name: string
@@ -26,13 +27,32 @@ const gradient = computed(() => {
return `conic-gradient(${stops.join(', ')})`
})
function percentOf(value: number): number {
if (total.value <= 0) return 0
return Math.round((value / total.value) * 100)
const AXIS = [250, 200, 150, 100, 50]
const AXIS_MAX = AXIS[0]
const MONTHS = ['1月', '2月', '3月', '4月', '5月']
interface PeriodOption {
label: string
value: 'month' | 'half'
}
const AXIS = [250, 200, 150, 100, 50]
const MONTHS = ['1月', '2月', '3月', '4月', '5月']
// 设计稿 #20 右上角切换:月度账单(激活)/ 上半
const PERIODS: PeriodOption[] = [
{ label: '月度账单', value: 'month' },
{ label: '上半', value: 'half' },
]
const period = ref<'month' | 'half'>('month')
function onPeriodChange(value: 'month' | 'half'): void {
period.value = value
}
// 柱高按 Y 轴量程绝对值换算(设计稿 #20:Y 轴 250 为满量程),非占比
function heightPercentOf(value: number): number {
if (value <= 0) return 0
return Math.min((value / AXIS_MAX) * 100, 100)
}
</script>
<template>
@@ -41,6 +61,18 @@ const MONTHS = ['1月', '2月', '3月', '4月', '5月']
业务类型分布
</h3>
<div class="finance-chart__switch">
<el-button
v-for="item in PERIODS"
:key="item.value"
class="finance-chart__switch-btn"
:class="{ 'is-active': period === item.value }"
@click="onPeriodChange(item.value)"
>
{{ item.label }}
</el-button>
</div>
<div class="finance-chart__body">
<ul class="finance-chart__axis" aria-hidden="true">
<li v-for="v in AXIS" :key="v" class="finance-chart__axis-item">
@@ -55,10 +87,10 @@ const MONTHS = ['1月', '2月', '3月', '4月', '5月']
<div class="finance-chart__bars">
<span
v-for="item in distribution"
:key="item.name"
v-for="(m, i) in MONTHS"
:key="m"
class="finance-chart__bar"
:style="{ height: `${Math.max(percentOf(item.value), 6)}%` }"
:style="{ height: `${heightPercentOf(distribution[i]?.value ?? 0)}%` }"
/>
</div>
@@ -78,6 +110,10 @@ const MONTHS = ['1月', '2月', '3月', '4月', '5月']
:key="item.name"
class="finance-chart__legend-item"
>
<i
class="finance-chart__dot"
:style="{ background: item.color }"
/>
<span class="finance-chart__name">{{ item.name }}</span>
</li>
</ul>
@@ -116,6 +152,42 @@ const MONTHS = ['1月', '2月', '3月', '4月', '5月']
}
}
&__switch {
.flex(row, flex-end, center);
gap: 8px;
position: absolute;
top: 25px;
right: 23px;
:deep(.el-button + .el-button) {
margin-left: 0;
}
}
&__switch-btn {
height: 24px;
margin-left: 0;
padding: 0 12px;
border: none;
border-radius: @radius-round;
background: @finance-switch-idle-bg;
font-size: 11px;
font-weight: 500;
line-height: 24px;
color: @finance-muted-text;
transition: color 0.2s, background 0.2s;
&:hover {
background: @finance-switch-idle-bg;
color: @finance-chart-switch-active;
}
&.is-active {
background: @finance-switch-active-bg;
color: @finance-chart-switch-active;
}
}
&__body {
.flex(row, flex-start, flex-start);
gap: 0;
@@ -126,7 +198,8 @@ const MONTHS = ['1月', '2月', '3月', '4月', '5月']
.flex(column, space-between, flex-start);
width: 34px;
height: 234px;
margin: 0;
// 设计稿 #20:Y 轴刻度文字左缘 x=103(卡左 70 + padding 23 + 10)
margin: 0 0 0 10px;
padding: 0;
list-style: none;
box-sizing: border-box;
@@ -164,7 +237,7 @@ const MONTHS = ['1月', '2月', '3月', '4月', '5月']
.flex(row, space-around, flex-end);
position: absolute;
inset: 0;
padding: 0 40px;
// 无横向 padding:柱子与下方月份标签同为 space-around,中心才能对齐(设计稿 #20 柱月对齐)
}
&__bar {
@@ -200,7 +273,8 @@ const MONTHS = ['1月', '2月', '3月', '4月', '5月']
.flex(row, flex-start, center);
gap: 27px;
margin: 41px 0 0;
padding: 0 0 0 662px;
// 设计稿 #20:图例文字左缘 x=809(卡左 70 + padding 23 + 704 + 圆点 6 + gap 6)
padding: 0 0 0 704px;
list-style: none;
}
@@ -212,6 +286,13 @@ const MONTHS = ['1月', '2月', '3月', '4月', '5月']
color: @finance-muted-text;
}
&__dot {
flex-shrink: 0;
width: 6px;
height: 6px;
border-radius: 50%;
}
&__name {
min-width: 0;
}

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { ElButton } from 'element-plus'
import type { InvoiceTitle } from '@/types/business/finance'
interface Props {
@@ -20,18 +21,30 @@ const emit = defineEmits<{
<span v-if="invoiceTitle.isDefault" class="invoice-title-card__badge">默认抬头</span>
<span v-else class="invoice-title-card__badge-placeholder" />
<div class="invoice-title-card__actions">
<button
<el-button
link
type="primary"
class="invoice-title-card__btn"
@click="emit('setDefault', invoiceTitle.id)"
>
{{ invoiceTitle.isDefault ? '取消默认' : '设为默认' }}
</button>
<button class="invoice-title-card__btn" @click="emit('delete', invoiceTitle.id)">
</el-button>
<el-button
link
type="primary"
class="invoice-title-card__btn"
@click="emit('delete', invoiceTitle.id)"
>
删除
</button>
<button class="invoice-title-card__btn" @click="emit('edit', invoiceTitle)">
</el-button>
<el-button
link
type="primary"
class="invoice-title-card__btn"
@click="emit('edit', invoiceTitle)"
>
编辑
</button>
</el-button>
</div>
</div>
@@ -104,6 +117,7 @@ const emit = defineEmits<{
&:hover {
color: @primary-strong;
background: transparent;
}
}

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElInput, ElRadio, ElButton } from 'element-plus'
import type { InvoiceTitle, InvoiceTitlePayload } from '@/types/business/finance'
interface Props {
@@ -50,41 +50,39 @@ function onCancel(): void {
<div class="title-dialog__field">
<span class="title-dialog__label">抬头类型</span>
<label class="title-dialog__radio">
<input v-model="form.type" type="radio" value="企业单位">
<el-radio v-model="form.type" value="企业单位">
企业单位
</label>
<label class="title-dialog__radio">
<input v-model="form.type" type="radio" value="非企业单位">
</el-radio>
<el-radio v-model="form.type" value="非企业单位">
非企业单位
</label>
</el-radio>
</div>
<div class="title-dialog__field">
<span class="title-dialog__label">发票抬头</span>
<input
<el-input
v-model="form.title"
class="title-dialog__input"
placeholder="请输入发票抬头"
>
/>
</div>
<div class="title-dialog__field">
<span class="title-dialog__label">税号</span>
<input
<el-input
v-model="form.taxNo"
class="title-dialog__input"
placeholder="请输入税号"
>
/>
</div>
<div class="title-dialog__footer">
<button class="title-dialog__btn" @click="onCancel">
<el-button class="title-dialog__btn" @click="onCancel">
取消
</button>
<button class="title-dialog__btn title-dialog__btn--confirm" @click="onSave">
</el-button>
<el-button class="title-dialog__btn title-dialog__btn--confirm" @click="onSave">
保存
</button>
</el-button>
</div>
</div>
</div>
@@ -119,6 +117,11 @@ function onCancel(): void {
.flex(row, flex-start, center);
gap: @spacing-sm;
margin-bottom: @spacing-base;
:deep(.el-radio__label) {
color: @text-color;
font-size: @font-size-base;
}
}
&__label {
@@ -128,31 +131,19 @@ function onCancel(): void {
color: @text-color;
}
&__radio {
.flex(row, flex-start, center);
gap: @spacing-xs;
font-size: @font-size-base;
color: @text-color;
cursor: pointer;
input {
accent-color: @primary-color;
}
}
&__input {
flex: 1;
height: 34px;
padding: 0 @spacing-sm;
border: 1px solid @border-color;
border-radius: @radius-base;
background: @bg-color-light;
font-size: @font-size-base;
outline: none;
transition: border-color 0.2s;
&:focus {
border-color: @primary-color;
:deep(.el-input__wrapper) {
height: 34px;
background: @bg-color-light;
box-shadow: none;
border-radius: @radius-base;
}
:deep(.el-input__inner) {
color: @text-color;
font-size: @font-size-base;
}
}

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElRadio, ElButton } from 'element-plus'
import type { PayMethod } from '@/types/business/finance'
interface Props {
@@ -47,13 +48,13 @@ function onCancel(): void {
class="pay-method-dialog__option"
:class="{ 'is-active': selected === 'wechat' }"
>
<input
<el-radio
v-model="selected"
type="radio"
value="wechat"
class="pay-method-dialog__radio"
>
<span class="pay-method-dialog__name">微信支付</span>
微信支付
</el-radio>
<span class="pay-method-dialog__tip text-secondary">推荐使用</span>
</label>
@@ -61,23 +62,23 @@ function onCancel(): void {
class="pay-method-dialog__option"
:class="{ 'is-active': selected === 'alipay' }"
>
<input
<el-radio
v-model="selected"
type="radio"
value="alipay"
class="pay-method-dialog__radio"
>
<span class="pay-method-dialog__name">支付宝支付</span>
支付宝支付
</el-radio>
<span class="pay-method-dialog__tip text-secondary">其他用户选择</span>
</label>
<div class="pay-method-dialog__footer">
<button class="pay-method-dialog__btn" @click="onCancel">
<el-button class="pay-method-dialog__btn" @click="onCancel">
取消
</button>
<button class="pay-method-dialog__btn pay-method-dialog__btn--confirm" @click="onConfirm">
</el-button>
<el-button class="pay-method-dialog__btn pay-method-dialog__btn--confirm" @click="onConfirm">
确认
</button>
</el-button>
</div>
</div>
</div>
@@ -129,13 +130,11 @@ function onCancel(): void {
}
&__radio {
accent-color: @primary-color;
}
&__name {
font-size: @font-size-base;
color: @heading-color;
font-weight: 500;
.el-radio__label {
color: @heading-color;
font-size: @font-size-base;
font-weight: 500;
}
}
&__tip {
@@ -158,7 +157,6 @@ function onCancel(): void {
color: @text-color;
font-size: @font-size-base;
transition: all 0.2s;
&:hover {
border-color: @primary-color;
color: @primary-color;

View File

@@ -13,10 +13,10 @@ const financeStore = useFinanceStore()
const { stats } = storeToRefs(financeStore)
const ENTRIES = [
{ label: '账单明细', to: '/finance/bills' },
{ label: '待支付', to: '/finance/bills' },
{ label: '发票申请', to: '/finance/invoices' },
{ label: '报表导出', to: '' },
{ key: 'bill', label: '账单明细', to: '/finance/bills' },
{ key: 'pending', label: '待支付', to: '/finance/bills' },
{ key: 'invoice', label: '发票申请', to: '/finance/invoices' },
{ key: 'export', label: '报表导出', to: '' },
] as const
function onPeriodChange(period: string): void {
@@ -49,20 +49,108 @@ onMounted(() => {
/>
<div class="finance__entries">
<template v-for="entry in ENTRIES" :key="entry.label">
<template v-for="entry in ENTRIES" :key="entry.key">
<RouterLink
v-if="entry.to"
:to="entry.to"
class="finance__entry"
>
{{ entry.label }}
<span
class="finance__entry-icon"
:class="`finance__entry-icon--${entry.key}`"
>
<svg
v-if="entry.key === 'bill'"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
x="4"
y="2.5"
width="12"
height="15"
rx="1.5"
/>
<path d="M7 7h6M7 10h6M7 13h4" />
</svg>
<svg
v-else-if="entry.key === 'pending'"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
x="2.5"
y="4.5"
width="15"
height="11"
rx="1.5"
/>
<path d="M2.5 8.5h15" />
</svg>
<svg
v-else-if="entry.key === 'invoice'"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
x="4"
y="2.5"
width="12"
height="15"
rx="1.5"
/>
<path d="M7.5 7h5M7.5 10.5h5M7.5 14h2.5" />
</svg>
<svg
v-else
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 13V4M6.5 7.5 10 4l3.5 3.5M4 15.5h12" />
</svg>
<i
v-if="entry.key === 'pending' && (stats?.pendingCount ?? 0) > 0"
class="finance__entry-badge"
>
{{ stats?.pendingCount }}
</i>
</span>
<span class="finance__entry-label">{{ entry.label }}</span>
</RouterLink>
<button
v-else
class="finance__entry"
@click="onExport"
>
{{ entry.label }}
<span class="finance__entry-icon finance__entry-icon--export">
<svg
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 13V4M6.5 7.5 10 4l3.5 3.5M4 15.5h12" />
</svg>
</span>
<span class="finance__entry-label">{{ entry.label }}</span>
</button>
</template>
</div>
@@ -87,7 +175,8 @@ onMounted(() => {
.flex(row, space-between, center);
flex: 0 0 672px;
height: 135px;
padding: 0 34px;
// 设计稿 #20:首入口中心 x=1272、尾中心 x=1756,两侧对称 padding 66px 命中
padding: 0 66px;
border: 1px solid @finance-card-line;
border-radius: @radius-base;
background: @white;
@@ -95,8 +184,10 @@ onMounted(() => {
}
&__entry {
.flex(row, center, center);
height: 100%;
.flex(column, center, center);
gap: 8px;
// 等宽保证 space-between 下四入口中心等距 161.5px(设计稿 #20)
width: 56px;
border: none;
background: transparent;
color: @finance-entry-text;
@@ -110,5 +201,56 @@ onMounted(() => {
color: @primary-strong;
}
}
&__entry-icon {
position: relative;
.flex(row, center, center);
width: 40px;
height: 40px;
border-radius: 10px;
svg {
width: 20px;
height: 20px;
}
&--bill {
background: @finance-entry-bill-bg;
color: @finance-entry-bill-icon;
}
&--pending {
background: @finance-entry-pending-bg;
color: @finance-pending;
}
&--invoice {
background: @finance-entry-invoice-bg;
color: @finance-entry-invoice-icon;
}
&--export {
background: @finance-entry-export-bg;
color: @finance-paid;
}
}
&__entry-badge {
position: absolute;
top: -7px;
// 设计稿 #20:badge 叠在图标右上内侧(右缘距图标右约 7px)
right: 7px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: @finance-entry-badge-bg;
color: @white;
font-size: 9px;
font-style: normal;
line-height: 16px;
text-align: center;
box-sizing: border-box;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { ElMessage } from 'element-plus'
import { ElMessage, ElButton, ElPagination } from 'element-plus'
import { ConfirmDialog } from '@/components'
import { useFinanceStore } from '@/stores/modules/finance'
import type { InvoiceTitle, InvoiceTitlePayload } from '@/types/business/finance'
@@ -24,7 +24,6 @@ const pageCount = computed(() => Math.max(1, Math.ceil(titles.value.length / PAG
const pagedTitles = computed(() =>
titles.value.slice((page.value - 1) * PAGE_SIZE, page.value * PAGE_SIZE),
)
const pages = computed(() => Array.from({ length: Math.max(5, pageCount.value) }, (_, i) => i + 1))
function go(next: number): void {
if (next < 1 || next > pageCount.value) return
@@ -89,9 +88,9 @@ onMounted(() => {
<h2 class="invoice-titles__title">
发票管理
</h2>
<button class="invoice-titles__create" @click="onCreate">
<el-button class="invoice-titles__create" @click="onCreate">
+ 新增抬头
</button>
</el-button>
</header>
<div class="invoice-titles__list">
@@ -108,23 +107,14 @@ onMounted(() => {
</p>
</div>
<nav class="invoice-titles__pager" aria-label="分页">
<button class="invoice-titles__page" :disabled="page <= 1" @click="go(page - 1)">
‹
</button>
<button
v-for="item in pages"
:key="item"
class="invoice-titles__page"
:class="{ 'is-active': item === page }"
@click="go(item)"
>
{{ item }}
</button>
<button class="invoice-titles__page" :disabled="page >= pageCount" @click="go(page + 1)">
›
</button>
</nav>
<el-pagination
class="invoice-titles__pager"
layout="prev, pager, next"
:page-size="PAGE_SIZE"
:total="titles.length"
:current-page="page"
@current-change="go"
/>
</div>
<InvoiceTitleDialog
@@ -197,6 +187,7 @@ onMounted(() => {
height: 34px;
margin-right: -5px;
border: none;
padding: 0;
border-radius: 4px;
background: @primary-strong;
color: @white;
@@ -223,33 +214,34 @@ onMounted(() => {
}
&__pager {
.flex(row, flex-end, center);
gap: 3.7px;
margin-top: 8px;
padding-right: 2.4px;
}
justify-content: flex-end;
&__page {
min-width: 21.45px;
height: 16px;
padding: 0 3px;
border: none;
border-radius: 0;
background: transparent;
color: @finance-muted-text;
font-size: 12px;
font-weight: 400;
transition: color 0.2s;
:deep(.el-pagination button),
:deep(.el-pagination .el-pager li) {
min-width: 21.45px;
height: 16px;
padding: 0 3px;
border: none;
border-radius: 0;
background: transparent;
color: @finance-muted-text;
font-size: 12px;
font-weight: 400;
transition: color 0.2s;
}
&:hover:not(:disabled) {
:deep(.el-pagination .el-pager li.is-active) {
color: @primary-strong;
font-weight: 500;
}
:deep(.el-pagination button:hover:not(:disabled)) {
color: @primary-strong;
}
&.is-active {
color: @primary-strong;
}
&:disabled {
:deep(.el-pagination button:disabled) {
cursor: not-allowed;
opacity: 0.5;
}

View File

@@ -2,7 +2,7 @@
import { computed, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { RouterLink } from 'vue-router'
import { ElMessage } from 'element-plus'
import { ElMessage, ElButton, ElTable, ElTableColumn } from 'element-plus'
import { FilterPanel, PaginationBar, StatusTag } from '@/components'
import { useFinanceStore } from '@/stores/modules/finance'
import type { Invoice, InvoiceStatus } from '@/types/business/finance'
@@ -69,13 +69,8 @@ function onSendEmail(): void {
ElMessage.success('发票已发送至绑定邮箱(演示)')
}
function onToggleSelect(id: string): void {
if (selectedIds.value.includes(id)) {
selectedIds.value = selectedIds.value.filter((item) => item !== id)
}
else {
selectedIds.value = [...selectedIds.value, id]
}
function onSelectionChange(rows: Invoice[]): void {
selectedIds.value = rows.map((row) => row.id)
}
onMounted(() => {
@@ -114,7 +109,7 @@ onMounted(() => {
</section>
<div class="invoices__tabs">
<button
<el-button
v-for="tab in STATUS_TABS"
:key="tab.value"
class="invoices__tab"
@@ -122,7 +117,7 @@ onMounted(() => {
@click="onTab(tab.value)"
>
{{ tab.label }}
</button>
</el-button>
</div>
<FilterPanel
@@ -133,67 +128,121 @@ onMounted(() => {
/>
<div class="invoices__table card">
<table class="invoices__table-el">
<thead>
<tr>
<th class="invoices__check">
多选
</th>
<th>序号</th>
<th>账单名称</th>
<th>账单号</th>
<th>交易流水号</th>
<th>项目名称</th>
<th>订单金额</th>
<th>状态</th>
<th>申请时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(invoice, index) in invoices" :key="invoice.id">
<td class="invoices__check">
<input
type="checkbox"
:checked="selectedIds.includes(invoice.id)"
@change="onToggleSelect(invoice.id)"
>
</td>
<td>{{ index + 1 }}</td>
<td>{{ invoice.planName }}</td>
<td>{{ invoice.billNo }}</td>
<td>{{ invoice.tradeNo ?? '—' }}</td>
<td>{{ invoice.projectName }}</td>
<td class="invoices__amount">¥{{ formatMoney(invoice.amount) }}</td>
<td>
<StatusTag :label="invoice.status" :variant="STATUS_VARIANTS[invoice.status]" />
</td>
<td>{{ invoice.applyTime }}</td>
<td class="invoices__ops">
<template v-if="invoice.status === '已开票'">
<button class="invoices__link" @click="onPreview">
<el-table
class="invoices__table-el"
:data="invoices"
row-key="id"
@selection-change="onSelectionChange"
>
<el-table-column
type="selection"
width="48"
/>
<el-table-column
type="index"
label="序号"
width="60"
/>
<el-table-column
prop="planName"
label="账单名称"
min-width="180"
show-overflow-tooltip
/>
<el-table-column
prop="billNo"
label="账单号"
width="150"
show-overflow-tooltip
/>
<el-table-column
prop="tradeNo"
label="交易流水号"
min-width="180"
show-overflow-tooltip
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="160"
show-overflow-tooltip
/>
<el-table-column
label="订单金额"
width="110"
>
<template #default="{ row }">
<span class="invoices__amount">¥{{ formatMoney((row as Invoice).amount) }}</span>
</template>
</el-table-column>
<el-table-column
label="状态"
width="90"
>
<template #default="{ row }">
<StatusTag
:label="(row as Invoice).status"
:variant="STATUS_VARIANTS[(row as Invoice).status]"
/>
</template>
</el-table-column>
<el-table-column
prop="applyTime"
label="申请时间"
width="140"
show-overflow-tooltip
/>
<el-table-column
label="操作"
width="180"
fixed="right"
>
<template #default="{ row }">
<span class="invoices__ops">
<template v-if="(row as Invoice).status === '已开票'">
<el-button
link
type="primary"
class="invoices__link"
@click="onPreview"
>
预览
</button>
<button class="invoices__link" @click="onDownload">
</el-button>
<el-button
link
type="primary"
class="invoices__link"
@click="onDownload"
>
下载
</button>
<button class="invoices__link" @click="onSendEmail">
</el-button>
<el-button
link
type="primary"
class="invoices__link"
@click="onSendEmail"
>
发送邮箱
</button>
</el-button>
</template>
<span v-else-if="invoice.status === '开票中'" class="invoices__dash">—</span>
<button v-else class="invoices__link" @click="onApply(invoice)">
<span v-else-if="(row as Invoice).status === '开票中'" class="invoices__dash">—</span>
<el-button
v-else
link
type="primary"
class="invoices__link"
@click="onApply(row as Invoice)"
>
申请开票
</button>
</td>
</tr>
<tr v-if="invoices.length === 0">
<td colspan="10" class="invoices__empty text-secondary">
暂无发票记录
</td>
</tr>
</tbody>
</table>
</el-button>
</span>
</template>
</el-table-column>
<template #empty>
<span class="invoices__empty text-secondary">暂无发票记录</span>
</template>
</el-table>
</div>
<PaginationBar
@@ -271,22 +320,27 @@ onMounted(() => {
}
&__tabs {
.flex(row, flex-start);
display: flex;
flex-direction: row;
align-items: flex-start;
gap: @spacing-sm;
}
&__tab {
height: 34px;
margin: 0;
padding: 0 @spacing-base;
border: 1px solid @border-color;
border-radius: @radius-base;
background: @white;
color: @text-color;
font-size: @font-size-base;
font-weight: 400;
transition: all 0.2s;
&:hover {
border-color: @primary-color;
background: @white;
color: @primary-color;
}
@@ -294,6 +348,12 @@ onMounted(() => {
border-color: @primary-color;
background: @primary-color;
color: @white;
&:hover {
border-color: @primary-color;
background: @primary-color;
color: @white;
}
}
}
@@ -307,32 +367,35 @@ onMounted(() => {
&__table-el {
width: 100%;
border-collapse: collapse;
font-size: @font-size-base;
color: @text-color;
th,
td {
padding: @spacing-sm;
text-align: left;
white-space: nowrap;
:deep(th.el-table__cell) {
background: @bg-color-light;
border-bottom: 1px solid @border-color;
}
th {
background: @bg-color-light;
color: @text-color-secondary;
font-size: @font-size-base;
font-weight: 500;
text-align: left;
}
tbody tr:hover {
:deep(td.el-table__cell) {
padding: @spacing-sm 0;
border-bottom: 1px solid @border-color;
font-size: @font-size-base;
}
:deep(.el-table__body tr:hover > td.el-table__cell) {
background: @bg-color-light;
}
}
&__check {
width: 48px;
text-align: center;
:deep(.el-table .cell) {
padding-left: @spacing-sm;
padding-right: @spacing-sm;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__amount {
@@ -341,17 +404,23 @@ onMounted(() => {
}
&__ops {
.flex(row, flex-start);
display: inline-flex;
flex-direction: row;
align-items: center;
gap: @spacing-sm;
}
&__link {
padding: 0;
border: none;
background: transparent;
color: @primary-color;
font-size: @font-size-base;
transition: color 0.2s;
&:hover {
color: @primary-hover;
background: transparent;
}
}

View File

@@ -2,6 +2,7 @@
import { computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useRoute, useRouter } from 'vue-router'
import { ElButton } from 'element-plus'
import { useFinanceStore } from '@/stores/modules/finance'
import { formatMoney } from '@/utils/format'
@@ -110,13 +111,12 @@ onMounted(() => {
</ul>
<div class="pay-result__footer">
<button
class="pay-result__back"
type="button"
@click="router.push('/finance/bills')"
>
返回
</button>
<el-button
class="pay-result__back"
@click="router.push('/finance/bills')"
>
返回
</el-button>
</div>
</section>
</template>
@@ -311,6 +311,7 @@ onMounted(() => {
width: 84px;
height: 32px;
border: none;
padding: 0;
border-radius: 6px;
background: @primary-strong;
color: @white;