Commit ce3e433a by yuzhenWang

修改样式

parent 5491ae58
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -36,7 +36,7 @@ function isActive(path: string): boolean {
<div class="h-[3px] bg-brand-600"></div>
<header class="sticky top-0 z-20 border-b border-silver-200/70 bg-white/80 backdrop-blur-lg">
<div class="mx-auto flex max-w-3xl items-center justify-between px-4 py-3">
<div class="text-[15px] font-semibold tracking-tight text-brand-700">{{ t('app.title') }}</div>
<div class="text-[18px] font-semibold tracking-tight text-brand-700">{{ t('app.title') }}</div>
<LoginBadge />
</div>
</header>
......@@ -67,4 +67,4 @@ function isActive(path: string): boolean {
</div>
</nav>
</div>
</template>
\ No newline at end of file
</template>
......@@ -145,6 +145,7 @@ export interface PolicyCommissionYear {
status: CommissionStatus
/** 保留/失效原因(财务登记,可能为空) */
statusDesc?: string
stage?: string
}
export interface PipelineItem {
......
......@@ -17,28 +17,28 @@ const open = ref(false)
const keyword = ref('')
const selectedName = computed(
() => brokers.value.find((b) => b.brokerBizId === selectedBizId.value)?.brokerName ?? ''
() => brokers.value.find(b => b.brokerBizId === selectedBizId.value)?.brokerName ?? ''
)
const filtered = computed(() => {
const kw = keyword.value.trim().toLowerCase()
const all = kw
? brokers.value.filter((b) => b.brokerName.toLowerCase().includes(kw))
? brokers.value.filter(b => b.brokerName.toLowerCase().includes(kw))
: brokers.value
// 后端已按 hasStatement 倒序返回,这里分组保持顺序
return {
withStatement: all.filter((b) => b.hasStatement),
rest: all.filter((b) => !b.hasStatement)
withStatement: all.filter(b => b.hasStatement),
rest: all.filter(b => !b.hasStatement)
}
})
onMounted(async () => {
try {
brokers.value = await get<BrokerOption[]>('/c/dev/brokers')
loaded.value = true
} catch (e) {
error.value = e instanceof Error ? e.message : 'error'
}
// try {
// brokers.value = await get<BrokerOption[]>('')
// loaded.value = true
// } catch (e) {
// error.value = e instanceof Error ? e.message : 'error'
// }
})
function toggle() {
......@@ -72,7 +72,7 @@ function pick(broker: BrokerOption) {
type="text"
placeholder="搜索姓名…"
class="w-full border-b border-amber-200 px-2.5 py-2 text-xs outline-none placeholder:text-slate-400"
>
/>
<ul class="max-h-[50vh] overflow-y-auto py-1">
<li
class="cursor-pointer px-2.5 py-1.5 text-slate-600 hover:bg-amber-50"
......@@ -81,7 +81,10 @@ function pick(broker: BrokerOption) {
— 未选择 —
</li>
<li v-if="filtered.withStatement.length" class="px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-amber-700">
<li
v-if="filtered.withStatement.length"
class="px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-amber-700"
>
有月结单 · {{ filtered.withStatement.length }}
</li>
<li
......@@ -94,7 +97,10 @@ function pick(broker: BrokerOption) {
{{ b.brokerName }}
</li>
<li v-if="filtered.rest.length" class="px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-slate-400">
<li
v-if="filtered.rest.length"
class="px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-slate-400"
>
其他有保单归属 · {{ filtered.rest.length }}
</li>
<li
......@@ -107,7 +113,12 @@ function pick(broker: BrokerOption) {
{{ b.brokerName }}
</li>
<li v-if="filtered.withStatement.length + filtered.rest.length === 0" class="px-2.5 py-2 text-slate-400">无匹配</li>
<li
v-if="filtered.withStatement.length + filtered.rest.length === 0"
class="px-2.5 py-2 text-slate-400"
>
无匹配
</li>
</ul>
</div>
......
......@@ -18,15 +18,24 @@ function statusClass(status: PipelineItem['status']): string {
</script>
<template>
<div class="rounded-2xl border border-silver-200 bg-white p-4 shadow-card">
<div
class="rounded-2xl border border-silver-200 bg-white p-4 shadow-card"
v-if="item.expectedAmount > 0"
>
<div class="flex items-center justify-between">
<div>
<div class="text-sm font-medium tracking-tight">{{ item.policyNo }}</div>
<div class="mt-1 text-xs text-slate-500">{{ t('pipeline.payoutMonth') }} {{ item.payoutMonth }}</div>
<div class="mt-1 text-xs text-slate-500">
{{ t('pipeline.payoutMonth') }} {{ item.payoutMonth }}
</div>
</div>
<div class="text-right">
<div class="tnum text-sm font-semibold">{{ formatDetailMoney(item.expectedAmount, item.currency) }}</div>
<div class="mt-1 text-xs font-medium" :class="statusClass(item.status)">{{ t(`stage.${item.status}`) }}</div>
<div class="tnum text-sm font-semibold">
总保费: {{ formatDetailMoney(item.expectedAmount, item.currency) }}
</div>
<div class="mt-1 text-xs font-medium" :class="statusClass(item.status)">
{{ t(`stage.${item.status}`) }}
</div>
</div>
</div>
</div>
......
......@@ -30,7 +30,9 @@ const statusTone = computed(() => (props.policy.status === 'LAPSED' ? 'neutral'
<div class="min-w-0">
<div class="flex items-center gap-2">
<span class="truncate text-sm font-medium tracking-tight">{{ policy.policyNo }}</span>
<Badge v-if="showStatus" :tone="statusTone">{{ t(`policyStatus.${policy.status}`) }}</Badge>
<Badge v-if="showStatus" :tone="statusTone">{{
t(`policyStatus.${policy.status}`)
}}</Badge>
</div>
<div class="mt-1 truncate text-xs text-silver-600">{{ policy.productName || '—' }}</div>
<div v-if="policy.insurerName" class="mt-0.5 truncate text-xs text-silver-400">
......@@ -39,7 +41,7 @@ const statusTone = computed(() => (props.policy.status === 'LAPSED' ? 'neutral'
</div>
<div class="shrink-0 text-right">
<div class="tnum text-sm font-semibold">
{{ formatDetailMoney(policy.fullPremium, policy.currency) }}
总保费: {{ formatDetailMoney(policy.fullPremium, policy.currency) }}
</div>
<div class="mt-0.5 text-xs text-silver-400">
{{ t('policies.myRatio') }} {{ policy.myRatio ? formatPercent(policy.myRatio) : '—' }}
......
......@@ -4,7 +4,11 @@ import { useI18n } from 'vue-i18n'
import { formatDetailMoney, formatHkd } from '@/utils/money'
import { formatPercent } from '@/utils/number'
import { calcPremiumTotals } from '@/utils/premium'
import type { PaymentFrequency, StatementDetail as StatementDetailType, StatementItem } from '@/api/types'
import type {
PaymentFrequency,
StatementDetail as StatementDetailType,
StatementItem
} from '@/api/types'
const props = defineProps<{
detail: StatementDetailType
......@@ -81,10 +85,13 @@ function rateText(rate: number): string {
</div>
</div>
</div>
<p class="mt-2 text-[11px] text-silver-400">{{ t('statement.amountHint') }}</p>
<p class="mt-2 text-[12px] text-silver-400">{{ t('statement.amountHint') }}</p>
<!-- 减项:没值就不显示 -->
<dl v-if="hasMpf || hasAdjustment" class="mt-3 space-y-2 border-t border-silver-100 pt-3 text-sm">
<dl
v-if="hasMpf || hasAdjustment"
class="mt-3 space-y-2 border-t border-silver-100 pt-3 text-sm"
>
<div v-if="hasMpf">
<div class="flex items-center justify-between gap-3">
<dt class="text-silver-600">{{ t('statement.mpfItem') }}</dt>
......@@ -103,7 +110,10 @@ function rateText(rate: number): string {
</div>
</dl>
<div v-if="detail.remark" class="mt-3 rounded-xl bg-silver-50 px-3 py-2 text-xs text-silver-600">
<div
v-if="detail.remark"
class="mt-3 rounded-xl bg-silver-50 px-3 py-2 text-xs text-silver-600"
>
<span class="font-medium">{{ t('statement.remark') }}</span>
<p class="mt-1 whitespace-pre-line leading-relaxed">{{ detail.remark }}</p>
</div>
......@@ -165,7 +175,7 @@ function rateText(rate: number): string {
:key="item.id"
:class="index ? 'mt-3 border-t border-silver-100 pt-3' : ''"
>
<div class="flex items-start justify-between gap-3">
<div class="flex items-start justify-between gap-3" v-if="item.originalAmount > 0">
<div class="min-w-0">
<div class="truncate text-sm font-medium">{{ item.policyNo }}</div>
<div class="mt-0.5 truncate text-xs text-silver-500">
......@@ -173,10 +183,10 @@ function rateText(rate: number): string {
</div>
</div>
<div class="shrink-0 text-right">
<div class="tnum text-sm font-semibold">
<div class="tnum text-sm font-semibold">≈ {{ formatHkd(item.hkdAmount) }}</div>
<div class="tnum mt-0.5 text-xs text-silver-400">
{{ formatDetailMoney(item.originalAmount, item.originalCurrency) }}
</div>
<div class="tnum mt-0.5 text-xs text-silver-400">≈ {{ formatHkd(item.hkdAmount) }}</div>
</div>
</div>
......@@ -193,7 +203,9 @@ function rateText(rate: number): string {
</div>
<div class="flex gap-1.5">
<dt class="shrink-0 text-silver-500">{{ t('statement.ratioShare') }}</dt>
<dd class="text-silver-700">{{ item.myRatio ? formatPercent(item.myRatio) : '—' }}</dd>
<dd class="text-silver-700">
{{ item.myRatio ? formatPercent(item.myRatio) : '—' }}
</dd>
</div>
<div class="flex gap-1.5">
<dt class="shrink-0 text-silver-500">{{ t('statement.policyRate') }}</dt>
......@@ -203,6 +215,7 @@ function rateText(rate: number): string {
<dt class="shrink-0 text-silver-500">{{ t('statement.premiumTerm') }}</dt>
<dd class="tnum text-silver-700">{{ premiumText(item) }}</dd>
</div>
<div v-if="item.remark" class="col-span-2 flex gap-1.5">
<dt class="shrink-0 text-silver-500">{{ t('statement.itemRemark') }}</dt>
<dd class="text-silver-600">{{ item.remark }}</dd>
......
......@@ -25,14 +25,14 @@ const { t } = useI18n()
<div class="min-w-0">
<div class="flex items-baseline gap-2">
<span class="text-sm font-medium tracking-tight">{{ statement.month || '—' }}</span>
<span class="text-[11px] text-silver-400">{{ t('statement.salaryMonth') }}</span>
<span class="text-[14px] text-silver-400">{{ t('statement.salaryMonth') }}</span>
</div>
<div class="mt-1 truncate text-xs text-silver-500">
{{ t('statement.payDate') }} {{ statement.payDate || '—' }}
</div>
</div>
<div class="shrink-0 text-right">
<div class="text-[11px] text-silver-500">
<div class="text-[14px] text-silver-500">
{{ t('statement.gross') }}
<span class="tnum">{{ formatHkd(statement.grossAmount) }}</span>
</div>
......
......@@ -20,12 +20,14 @@ const heroCard = computed(() =>
: null
)
const restCards = computed(() => {
type RestCard = { label: string; value: string; to?: string }
const restCards = computed<RestCard[]>(() => {
if (!metrics.value) return []
return [
{ label: t('metric.taxYearPaid'), value: formatHkd(metrics.value.taxYearPaidHkd) },
{ label: t('metric.pipeline'), value: formatHkd(metrics.value.pipelineHkd) },
{ label: t('metric.inforceCount'), value: String(metrics.value.inforceCount) },
{ label: t('metric.pipeline'), value: formatHkd(metrics.value.pipelineHkd), to: '/pipeline' },
{ label: t('metric.inforceCount'), value: String(metrics.value.inforceCount), to: '/policies' },
{ label: t('metric.firstYearFyp'), value: formatHkd(metrics.value.firstYearFypHkd) }
]
})
......@@ -63,9 +65,19 @@ onMounted(load)
<ApiLoader :loading="loading" :error="error" @retry="load">
<MetricCard v-if="heroCard" feature :label="heroCard.label" :value="heroCard.value" />
<div class="mt-3 grid grid-cols-2 gap-3">
<MetricCard v-for="card in restCards" :key="card.label" :label="card.label" :value="card.value" />
<!-- <MetricCard
v-for="card in restCards"
:key="card.label"
:label="card.label"
:value="card.value"
/> -->
<template v-for="card in restCards" :key="card.label">
<router-link v-if="card.to" :to="card.to" class="block">
<MetricCard :label="card.label" :value="card.value" />
</router-link>
<MetricCard v-else :label="card.label" :value="card.value" />
</template>
</div>
<router-link
v-if="metrics"
to="/customers"
......@@ -73,37 +85,58 @@ onMounted(load)
>
<div>
<div class="text-xs text-slate-500">{{ t('metric.myCustomers') }}</div>
<div class="tnum mt-2 text-xl font-semibold tracking-tight text-slate-900">{{ metrics.caseCount }}</div>
<div class="tnum mt-2 text-xl font-semibold tracking-tight text-slate-900">
{{ metrics.caseCount }}
</div>
</div>
<div class="flex items-center gap-1 text-xs text-slate-400">
{{ t('customer.followingHint', { count: metrics.followingCount }) }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
>
<polyline points="9 18 15 12 9 6" />
</svg>
</div>
</router-link>
<section class="mt-7">
<!-- <section class="mt-7">
<div class="mb-3 flex items-center gap-2">
<h2 class="text-[13px] font-semibold tracking-wide text-silver-600">{{ t('dashboard.actions') }}</h2>
<h2 class="text-[13px] font-semibold tracking-wide text-silver-600">
{{ t('dashboard.actions') }}
</h2>
<span
v-if="todoDataSource === 'mock'"
class="rounded-full bg-amber-50 px-2 py-0.5 text-[11px] font-medium text-amber-700"
>{{ t('todo.sample') }}</span>
>{{ t('todo.sample') }}</span
>
</div>
<div class="mb-3 flex flex-wrap gap-2 text-xs text-slate-500">
<span class="rounded-full bg-slate-100 px-2.5 py-1">{{ t('todo.renewal') }} {{ countByType.renewal }}</span>
<span class="rounded-full bg-slate-100 px-2.5 py-1">{{ t('todo.lapse') }} {{ countByType.lapse }}</span>
<span class="rounded-full bg-slate-100 px-2.5 py-1">{{ t('todo.birthday') }} {{ countByType.birthday }}</span>
<span class="rounded-full bg-slate-100 px-2.5 py-1"
>{{ t('todo.renewal') }} {{ countByType.renewal }}</span
>
<span class="rounded-full bg-slate-100 px-2.5 py-1"
>{{ t('todo.lapse') }} {{ countByType.lapse }}</span
>
<span class="rounded-full bg-slate-100 px-2.5 py-1"
>{{ t('todo.birthday') }} {{ countByType.birthday }}</span
>
</div>
<div class="space-y-3">
1111
<TodoCard v-for="todo in todos" :key="todo.id" :todo="todo" />
<div class="rounded-2xl border border-dashed border-silver-200 bg-white/60 p-4">
<div class="text-sm font-medium">{{ t('todo.claim') }}</div>
<div class="mt-1 text-xs text-slate-400">{{ t('todo.claimPlaceholder') }}</div>
</div>
</div>
</section>
</section> -->
</ApiLoader>
</div>
</template>
\ No newline at end of file
</template>
......@@ -42,7 +42,11 @@ onMounted(load)
<template>
<div class="space-y-4">
<div class="rounded-2xl border border-amber-100 bg-amber-50 px-4 py-3 text-xs font-medium text-amber-700">{{ t('pipeline.adjustNote') }}</div>
<div
class="rounded-2xl border border-amber-100 bg-amber-50 px-4 py-3 text-xs font-medium text-amber-700"
>
{{ t('pipeline.adjustNote') }}
</div>
<div class="grid grid-cols-2 gap-3">
<label class="block">
......@@ -51,26 +55,58 @@ onMounted(load)
v-model="policyQuery"
type="search"
:placeholder="t('pipeline.filterPolicyPlaceholder')"
class="w-full rounded-xl border border-silver-200 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition-colors placeholder:text-silver-400 focus:border-brand-200"
>
class="w-full rounded-xl border border-silver-200 bg-white px-3 py-2 text-base text-slate-900 outline-none transition-colors placeholder:text-silver-400 focus:border-brand-200"
/>
</label>
<label class="block">
<!-- <label class="block">
<span class="mb-1 block text-xs text-slate-500">{{ t('pipeline.filterMonth') }}</span>
<select
v-model="selectedMonth"
class="w-full rounded-xl border border-silver-200 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition-colors focus:border-brand-200"
class="w-full rounded-xl border border-silver-200 bg-white px-3 py-2 text-slate-900 outline-none transition-colors focus:border-brand-200"
>
<option value="">{{ t('pipeline.allMonths') }}</option>
<option v-for="month in months" :key="month" :value="month">{{ month }}</option>
</select>
</label> -->
<label class="block">
<span class="mb-1 block text-xs text-slate-500">{{ t('pipeline.filterMonth') }}</span>
<div class="relative">
<select
v-model="selectedMonth"
class="w-full appearance-none rounded-xl border border-silver-200 bg-white px-3 py-2 pr-10 text-base text-slate-900 outline-none transition-colors focus:border-brand-200"
>
<option value="">{{ t('pipeline.allMonths') }}</option>
<option v-for="month in months" :key="month" :value="month">{{ month }}</option>
</select>
<!-- 自定义箭头:right-3 就是箭头离右边框的间隔,想大改这里 -->
<svg
class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-silver-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
</label>
</div>
<ApiLoader :loading="loading" :error="error" :empty="!loading && !error && items.length === 0" @retry="load">
<ApiLoader
:loading="loading"
:error="error"
:empty="!loading && !error && items.length === 0"
@retry="load"
>
<div v-if="filtered.length" class="space-y-3">
<PipelineCard v-for="item in filtered" :key="item.id" :item="item" />
</div>
<div v-else class="py-10 text-center text-sm text-slate-400">{{ t('pipeline.noResult') }}</div>
<div v-else class="py-10 text-center text-sm text-slate-400">
{{ t('pipeline.noResult') }}
</div>
</ApiLoader>
</div>
</template>
......@@ -18,8 +18,8 @@ const modalError = ref<string | null>(null)
const commissions = ref<PolicyCommissionYear[]>([])
const selectedPolicyNo = ref('')
const inforce = computed(() => policies.value.filter((p) => p.status === 'INFORCE'))
const inactive = computed(() => policies.value.filter((p) => p.status !== 'INFORCE'))
const inforce = computed(() => policies.value.filter(p => p.status === 'INFORCE'))
const inactive = computed(() => policies.value.filter(p => p.status !== 'INFORCE'))
async function load() {
loading.value = true
......@@ -59,19 +59,38 @@ onMounted(load)
</script>
<template>
<ApiLoader :loading="loading" :error="error" :empty="!loading && !error && policies.length === 0" @retry="load">
<ApiLoader
:loading="loading"
:error="error"
:empty="!loading && !error && policies.length === 0"
@retry="load"
>
<div class="space-y-6">
<section>
<h2 class="mb-3 text-[13px] font-semibold tracking-wide text-silver-600">{{ t('policies.inforce') }}</h2>
<h2 class="mb-3 text-[13px] font-semibold tracking-wide text-silver-600">
{{ t('policies.inforce') }}
</h2>
<div class="space-y-3">
<PolicyCard v-for="policy in inforce" :key="policy.policyNo" :policy="policy" @select="openCommissions" />
<PolicyCard
v-for="policy in inforce"
:key="policy.policyNo"
:policy="policy"
@select="openCommissions"
/>
</div>
</section>
<section v-if="inactive.length">
<h2 class="mb-3 text-[13px] font-semibold tracking-wide text-silver-600">{{ t('policies.inactive') }}</h2>
<h2 class="mb-3 text-[13px] font-semibold tracking-wide text-silver-600">
{{ t('policies.inactive') }}
</h2>
<div class="space-y-3">
<PolicyCard v-for="policy in inactive" :key="policy.policyNo" :policy="policy" @select="openCommissions" />
<PolicyCard
v-for="policy in inactive"
:key="policy.policyNo"
:policy="policy"
@select="openCommissions"
/>
</div>
</section>
</div>
......@@ -90,10 +109,16 @@ onMounted(load)
:key="`${item.year}-${item.status}-${index}`"
class="flex items-center justify-between gap-3 text-sm"
>
<span>{{ item.year }} · {{ item.type }}</span>
<span
>{{ item.year }} · {{ item.type }}
<span v-if="item.status == 'PIPELINE'">(预估)</span>
</span>
<span class="text-right">
<span class="tnum">{{ formatDetailMoney(item.amount, item.currency) }}</span> · {{ t(`stage.${item.status}`) }}
<span v-if="item.statusDesc" class="block text-xs text-slate-400">{{ item.statusDesc }}</span>
<span class="tnum">{{ formatDetailMoney(item.amount, item.currency) }}</span> ·
{{ t(`stage.${item.status}`) }}
<span v-if="item.statusDesc" class="block text-xs text-slate-400">{{
item.statusDesc
}}</span>
</span>
</div>
</div>
......
......@@ -47,6 +47,11 @@ export default {
borderRadius: {
xl: '0.75rem',
'2xl': '1rem'
},
fontSize: {
xs: ['.9375rem', { lineHeight: '1.25rem' }], // 13px + 20px 行高
sm: ['1.125rem'],
lg: ['1.25rem', { lineHeight: '1.25rem' }] // 13px + 20px 行高
}
}
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment