20260410罗总新需求修改
This commit is contained in:
parent
860bd0161b
commit
99684c04f2
92
HomeHeroBrandTicker.vue
Normal file
92
HomeHeroBrandTicker.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
const brandName = '慧众易'
|
||||
const messages = ['真容易', '算费真容易', '算费不熬夜', '算费不费力', '不熬夜,不费力']
|
||||
const messageIndex = ref(0)
|
||||
let rotationTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const activeMessage = computed(() => {
|
||||
if (!messages.length) return ''
|
||||
return messages[((messageIndex.value % messages.length) + messages.length) % messages.length]
|
||||
})
|
||||
|
||||
const stopRotation = () => {
|
||||
if (rotationTimer == null) return
|
||||
clearInterval(rotationTimer)
|
||||
rotationTimer = null
|
||||
}
|
||||
|
||||
const startRotation = () => {
|
||||
stopRotation()
|
||||
if (messages.length <= 1) return
|
||||
rotationTimer = setInterval(() => {
|
||||
messageIndex.value = (messageIndex.value + 1) % messages.length
|
||||
}, 2400)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startRotation()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopRotation()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hero-brand-ticker">
|
||||
<span class="hero-brand-ticker__brand">{{ brandName }}</span>
|
||||
<span class="hero-brand-ticker__divider" />
|
||||
<Transition name="hero-brand-message" mode="out-in">
|
||||
<span :key="messageIndex" class="hero-brand-ticker__message">{{ activeMessage }}</span>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-brand-ticker {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 999px;
|
||||
backdrop-filter: blur(8px);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.hero-brand-ticker__brand {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.28em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hero-brand-ticker__divider {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.hero-brand-ticker__message {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hero-brand-message-enter-active,
|
||||
.hero-brand-message-leave-active {
|
||||
transition: opacity 0.22s ease, transform 0.22s ease;
|
||||
}
|
||||
|
||||
.hero-brand-message-enter-from,
|
||||
.hero-brand-message-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
</style>
|
||||
75
HwzPromoBanner.vue
Normal file
75
HwzPromoBanner.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<section class="hwz-promo-banner" aria-label="慧众易宣传文案">
|
||||
<div class="hwz-promo-banner__inner">
|
||||
<p class="hwz-promo-banner__eyebrow">智算费用 即点即出</p>
|
||||
<h2 class="hwz-promo-banner__title">您的时间留给创造</h2>
|
||||
<p class="hwz-promo-banner__brand">和慧众易</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hwz-promo-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 320px;
|
||||
padding: 32px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(254, 240, 138, 0.95), transparent 38%),
|
||||
linear-gradient(135deg, #0f172a 0%, #1d4ed8 48%, #0f766e 100%);
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hwz-promo-banner__inner {
|
||||
width: min(100%, 720px);
|
||||
padding: 40px;
|
||||
color: #f8fafc;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 24px;
|
||||
backdrop-filter: blur(14px);
|
||||
background: rgba(15, 23, 42, 0.26);
|
||||
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.28);
|
||||
}
|
||||
|
||||
.hwz-promo-banner__eyebrow {
|
||||
margin: 0 0 16px;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: #dbeafe;
|
||||
}
|
||||
|
||||
.hwz-promo-banner__title {
|
||||
margin: 0;
|
||||
font-size: clamp(34px, 7vw, 64px);
|
||||
line-height: 1.08;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hwz-promo-banner__brand {
|
||||
margin: 20px 0 0;
|
||||
font-size: clamp(20px, 3vw, 28px);
|
||||
font-weight: 500;
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hwz-promo-banner {
|
||||
min-height: 260px;
|
||||
padding: 20px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.hwz-promo-banner__inner {
|
||||
padding: 28px 22px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.hwz-promo-banner__eyebrow {
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
public/disclaimer.html
Normal file
86
public/disclaimer.html
Normal file
@ -0,0 +1,86 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>免责声明</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: "Microsoft YaHei", "PingFang SC", "Noto Sans SC", sans-serif;
|
||||
color: #0f172a;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(59, 130, 246, 0.12), transparent 32%),
|
||||
linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%);
|
||||
}
|
||||
|
||||
.page {
|
||||
width: min(100%, 880px);
|
||||
margin: 0 auto;
|
||||
padding: 56px 20px 72px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 32px 28px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.08);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(28px, 5vw, 40px);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 20px 0 18px;
|
||||
background: linear-gradient(90deg, rgba(148, 163, 184, 0), rgba(148, 163, 184, 0.7), rgba(148, 163, 184, 0));
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.9;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 18px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<section class="card">
|
||||
<p class="eyebrow">DISCLAIMER</p>
|
||||
<h1>免责声明</h1>
|
||||
<div class="divider"></div>
|
||||
<p>本计算工具由众为工程咨询有限公司提供免费技术支持。</p>
|
||||
<p class="signature">众为工程咨询有限公司</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@ -302,6 +302,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
||||
minWidth: 90,
|
||||
maxWidth: 140,
|
||||
colSpan: params => (params.data?.rowType === 'total' ? 2 : 1),
|
||||
cellClassRules: {
|
||||
'ag-summary-label-cell': params => params.data?.rowType === 'total'
|
||||
},
|
||||
valueFormatter: params => {
|
||||
if (params.data?.rowType === 'total') return params.data.name || t('htSummary.total')
|
||||
return typeof params.value === 'string' ? params.value : ''
|
||||
|
||||
@ -16,10 +16,16 @@ const XM_DB_KEY = computed(() => {
|
||||
})
|
||||
const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
|
||||
const { t } = useI18n()
|
||||
const titleText = computed(() => `${t('htInfo.scaleDetailTitle')}:${t('htInfo.scaleDetailHint')}`)
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CommonAgGrid :title="t('htInfo.scaleDetailTitle')" :dbKey="DB_KEY" :xmInfoKey="XM_DB_KEY" :base-info-key="BASE_INFO_KEY"/>
|
||||
<CommonAgGrid
|
||||
:title="titleText"
|
||||
:dbKey="DB_KEY"
|
||||
:xmInfoKey="XM_DB_KEY"
|
||||
:base-info-key="BASE_INFO_KEY"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -50,8 +50,8 @@ const SERVICE_SELECTOR_ROWS: string[][] = [
|
||||
['0', '2', '3', '4'],
|
||||
['6', '7', '10', '8', '9', '11', '12', '13'],
|
||||
['37', '38', '39', '40'],
|
||||
['21', '22', '23', '24', '25', '26'],
|
||||
['15', '16', '17', '18', '19', '20'],
|
||||
['21', '22', '23', '24', '25', '26'],
|
||||
['29', '30', '31', '32', '33', '34', '35', '36'],
|
||||
['41', '42', '43', '44']
|
||||
]
|
||||
@ -627,6 +627,9 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
field: 'code',
|
||||
minWidth: 50,
|
||||
maxWidth: 100,
|
||||
cellClassRules: {
|
||||
'ag-summary-label-cell': params => Boolean(params.data && isFixedRow(params.data))
|
||||
},
|
||||
valueGetter: params => {
|
||||
if (!params.data) return ''
|
||||
if (isFixedRow(params.data)) return t('htZxFw.subtotal')
|
||||
|
||||
@ -1191,7 +1191,10 @@ const processCellFromClipboard = (params: any) => {
|
||||
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-sm font-semibold text-foreground">{{ t('pricingPane.investment.title') }}</h3>
|
||||
<div class="flex min-w-0 items-center gap-1.5">
|
||||
<h3 class="shrink-0 text-sm font-semibold text-foreground">{{ t('pricingPane.investment.title') }}</h3>
|
||||
<span class="min-w-0 text-xs leading-5 text-muted-foreground">{{ t('pricingPane.investment.titleHint') }}</span>
|
||||
</div>
|
||||
<div v-if="isMutipleService" class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground">{{ t('pricingPane.projectCount') }}</span>
|
||||
<NumberFieldRoot
|
||||
|
||||
@ -285,6 +285,9 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
width: 120,
|
||||
pinned: 'left',
|
||||
colSpan: params => (params.node?.rowPinned ? 2 : 1),
|
||||
cellClassRules: {
|
||||
'ag-summary-label-cell': params => Boolean(params.node?.rowPinned)
|
||||
},
|
||||
valueFormatter: params => (params.node?.rowPinned ? t('workloadPricing.total') : params.value || '')
|
||||
},
|
||||
{
|
||||
|
||||
@ -376,6 +376,9 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
|
||||
width: 100,
|
||||
pinned: 'left',
|
||||
colSpan: params => (params.node?.rowPinned ? 2 : 1),
|
||||
cellClassRules: {
|
||||
'ag-summary-label-cell': params => Boolean(params.node?.rowPinned)
|
||||
},
|
||||
valueFormatter: params => (params.node?.rowPinned ? t('hourlyFeeGrid.total') : params.value || '')
|
||||
},
|
||||
{
|
||||
|
||||
@ -279,6 +279,9 @@ const columnDefs: ColDef<FeeRow>[] = [
|
||||
: typeof params.node?.rowIndex === 'number'
|
||||
? params.node.rowIndex + 1
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-summary-label-cell': params => isSubtotalRow(params.data)
|
||||
},
|
||||
colSpan: params => (isSubtotalRow(params.data) ? 2 : 1)
|
||||
},
|
||||
{
|
||||
|
||||
@ -498,6 +498,7 @@ const columnDefs: ColDef<FeeMethodRow>[] = [
|
||||
? ''
|
||||
: 'editable-cell-line',
|
||||
cellClassRules: {
|
||||
'ag-summary-label-cell': params => isSummaryRow(params.data),
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
interface ServiceItem {
|
||||
@ -21,6 +21,13 @@ const { t } = useI18n()
|
||||
|
||||
const selectedSet = computed(() => new Set(props.modelValue))
|
||||
const serviceById = computed(() => new Map(props.services.map(item => [item.id, item])))
|
||||
const firstRowIds = computed(() => {
|
||||
const rows = Array.isArray(props.serviceRows) ? props.serviceRows : []
|
||||
if (rows.length === 0) return [] as string[]
|
||||
const firstRow = Array.isArray(rows[0]) ? rows[0] : []
|
||||
return firstRow.filter(id => serviceById.value.has(id))
|
||||
})
|
||||
const firstRowIdSet = computed(() => new Set(firstRowIds.value))
|
||||
const groupedRows = computed(() => {
|
||||
const rows = Array.isArray(props.serviceRows) ? props.serviceRows : []
|
||||
if (rows.length === 0) return [] as ServiceItem[][]
|
||||
@ -42,11 +49,37 @@ const groupedRows = computed(() => {
|
||||
|
||||
const toggleService = (id: string, checked: boolean) => {
|
||||
const next = new Set(props.modelValue)
|
||||
if (checked) next.add(id)
|
||||
else next.delete(id)
|
||||
if (checked) {
|
||||
if (firstRowIdSet.value.has(id)) {
|
||||
firstRowIds.value.forEach(firstId => next.delete(firstId))
|
||||
}
|
||||
next.add(id)
|
||||
} else {
|
||||
next.delete(id)
|
||||
}
|
||||
emit('update:modelValue', props.services.map(item => item.id).filter(itemId => next.has(itemId)))
|
||||
}
|
||||
|
||||
const isFirstRowDisabled = (id: string) => {
|
||||
if (!firstRowIdSet.value.has(id)) return false
|
||||
for (const selectedId of props.modelValue) {
|
||||
if (selectedId !== id && firstRowIdSet.value.has(selectedId)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, firstRowIds.value] as const,
|
||||
() => {
|
||||
const firstSelected = props.modelValue.filter(id => firstRowIdSet.value.has(id))
|
||||
if (firstSelected.length <= 1) return
|
||||
const keepId = firstRowIds.value.find(id => firstSelected.includes(id)) || firstSelected[0]
|
||||
const next = props.modelValue.filter(id => !firstRowIdSet.value.has(id) || id === keepId)
|
||||
emit('update:modelValue', next)
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
const clearAll = () => {
|
||||
emit('update:modelValue', [])
|
||||
}
|
||||
@ -55,7 +88,10 @@ const clearAll = () => {
|
||||
<template>
|
||||
<div class="rounded-lg border bg-card p-2.5 shadow-sm">
|
||||
<div class="mb-1 flex items-center justify-between gap-2">
|
||||
<label class="block text-[11px] font-medium text-foreground leading-none">{{ t('serviceSelector.title') }}</label>
|
||||
<div class="flex min-w-0 items-center gap-1.5">
|
||||
<label class="block shrink-0 text-sm font-semibold leading-none text-slate-900">{{ t('serviceSelector.title') }}</label>
|
||||
<span class="min-w-0 text-xs leading-5 text-muted-foreground">{{ t('serviceSelector.titleHint') }}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="cursor-pointer h-6 rounded-md border px-2 text-[13px] text-muted-foreground transition hover:bg-accent"
|
||||
@ -69,17 +105,26 @@ const clearAll = () => {
|
||||
<div
|
||||
v-for="(row, rowIndex) in groupedRows"
|
||||
:key="`service-row-${rowIndex}`"
|
||||
class="flex flex-wrap items-start gap-1"
|
||||
class="flex flex-wrap items-start gap-1 border-b border-slate-200 pb-1.5 last:border-b-0 last:pb-0"
|
||||
>
|
||||
<label
|
||||
v-for="item in row"
|
||||
:key="item.id"
|
||||
class="inline-flex w-fit max-w-full cursor-pointer items-start gap-1.5 rounded-md border px-2 py-1 text-[11px] leading-4 hover:bg-muted/60"
|
||||
:class="[
|
||||
'inline-flex w-fit max-w-full items-start gap-1.5 rounded-md border px-2 py-1 text-[11px] leading-4 transition',
|
||||
isFirstRowDisabled(item.id)
|
||||
? 'cursor-not-allowed border-slate-300 bg-slate-100/80 text-slate-400 opacity-80'
|
||||
: 'cursor-pointer hover:bg-muted/60'
|
||||
]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="mt-0.5"
|
||||
:class="[
|
||||
'mt-0.5',
|
||||
isFirstRowDisabled(item.id) ? 'cursor-not-allowed accent-slate-300' : 'cursor-pointer'
|
||||
]"
|
||||
:checked="selectedSet.has(item.id)"
|
||||
:disabled="isFirstRowDisabled(item.id)"
|
||||
@change="toggleService(item.id, ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<span class="text-muted-foreground shrink-0">{{ item.code }}</span>
|
||||
|
||||
@ -22,6 +22,8 @@ import {
|
||||
ToastViewport
|
||||
} from 'reka-ui'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { CircleHelp } from 'lucide-vue-next'
|
||||
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
|
||||
|
||||
|
||||
@ -329,6 +331,8 @@ const props = defineProps<{
|
||||
dbKey: string
|
||||
xmInfoKey?: string | null
|
||||
baseInfoKey?: string
|
||||
titleHint?: string
|
||||
titleHintAria?: string
|
||||
}>()
|
||||
|
||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
@ -431,6 +435,9 @@ const autoGroupColumnDef: ColDef = {
|
||||
headerName: t('pricingScale.columns.majorGroup'),
|
||||
minWidth: 200,
|
||||
flex: 2,
|
||||
cellClassRules: {
|
||||
'ag-summary-label-cell': params => Boolean(params.node?.rowPinned)
|
||||
},
|
||||
cellRendererParams: {
|
||||
suppressCount: true
|
||||
},
|
||||
@ -756,10 +763,24 @@ onMounted(() => {
|
||||
<div class="h-full">
|
||||
<div class="rounded-lg border bg-card xmMx scroll-mt-3 flex flex-col overflow-hidden h-full">
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<h3
|
||||
class="text-sm font-semibold text-foreground cursor-pointer select-none transition-colors hover:text-primary">
|
||||
{{ props.title }}
|
||||
</h3>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<h3
|
||||
class="text-sm font-semibold text-foreground cursor-pointer select-none transition-colors hover:text-primary">
|
||||
{{ props.title }}
|
||||
</h3>
|
||||
<TooltipRoot v-if="props.titleHint">
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="props.titleHintAria || props.titleHint"
|
||||
class="inline-flex h-5 w-5 items-center justify-center text-muted-foreground/90 transition hover:text-foreground"
|
||||
>
|
||||
<CircleHelp class="h-4 w-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">{{ props.titleHint }}</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</div>
|
||||
<!-- <div class="flex items-center gap-2">
|
||||
<span class=" text-xs text-muted-foreground">简要计算</span>
|
||||
<SwitchRoot
|
||||
|
||||
@ -104,20 +104,6 @@ const projectIndustryLabel = computed(() => {
|
||||
if (!target) return ''
|
||||
return getIndustryDisplayName(target, locale.value) || ''
|
||||
})
|
||||
const heroBrandName = computed(() => t('home.cards.heroBrand'))
|
||||
const heroBrandMessages = computed(() => [
|
||||
t('home.cards.heroBrandMessage1'),
|
||||
t('home.cards.heroBrandMessage2'),
|
||||
t('home.cards.heroBrandMessage3'),
|
||||
t('home.cards.heroBrandMessage4'),
|
||||
t('home.cards.heroBrandMessage5')
|
||||
].filter(Boolean))
|
||||
const heroBrandMessageIndex = ref(0)
|
||||
const activeHeroBrandMessage = computed(() => {
|
||||
const options = heroBrandMessages.value
|
||||
if (!options.length) return ''
|
||||
return options[((heroBrandMessageIndex.value % options.length) + options.length) % options.length]
|
||||
})
|
||||
const localeBadge = computed(() => (locale.value === 'en-US' ? 'EN' : '中'))
|
||||
const toggleLocale = () => {
|
||||
const next = locale.value === 'en-US' ? 'zh-CN' : 'en-US'
|
||||
@ -407,29 +393,19 @@ const handleHomeVisibilityChange = () => {
|
||||
handleHomeWindowFocus()
|
||||
}
|
||||
|
||||
let heroBrandMessageTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const stopHeroBrandMessageRotation = () => {
|
||||
if (heroBrandMessageTimer == null) return
|
||||
clearInterval(heroBrandMessageTimer)
|
||||
heroBrandMessageTimer = null
|
||||
}
|
||||
|
||||
const startHeroBrandMessageRotation = () => {
|
||||
stopHeroBrandMessageRotation()
|
||||
if (heroBrandMessages.value.length <= 1) return
|
||||
heroBrandMessageTimer = setInterval(() => {
|
||||
const total = heroBrandMessages.value.length
|
||||
if (total <= 1) return
|
||||
heroBrandMessageIndex.value = (heroBrandMessageIndex.value + 1) % total
|
||||
}, 2400)
|
||||
const openDisclaimerPage = () => {
|
||||
try {
|
||||
const href = new URL('disclaimer.html', window.location.href).toString()
|
||||
window.open(href, '_blank', 'noopener')
|
||||
} catch {
|
||||
window.open('./disclaimer.html', '_blank', 'noopener')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void refreshExistingProjects()
|
||||
void loadProjectDefaults()
|
||||
void loadQuickDefaults()
|
||||
startHeroBrandMessageRotation()
|
||||
window.addEventListener('focus', handleHomeWindowFocus)
|
||||
document.addEventListener('visibilitychange', handleHomeVisibilityChange)
|
||||
try {
|
||||
@ -454,7 +430,6 @@ onMounted(() => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopExistingProjectPolling()
|
||||
stopHeroBrandMessageRotation()
|
||||
window.removeEventListener('focus', handleHomeWindowFocus)
|
||||
document.removeEventListener('visibilitychange', handleHomeVisibilityChange)
|
||||
})
|
||||
@ -501,15 +476,8 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<h2 class="relative mt-8 text-2xl font-semibold leading-tight tracking-tight lg:text-3xl">{{ t('home.cards.heroTitle') }}</h2>
|
||||
<p class="relative mt-2 text-sm text-red-200/90">{{ t('home.cards.heroSubTitle') }}</p>
|
||||
<div class="relative mt-4 inline-flex max-w-full items-center gap-3 rounded-full border border-white/15 bg-white/10 px-4 py-2 text-xs text-white/90 backdrop-blur-sm">
|
||||
<span class="shrink-0 font-semibold tracking-[0.28em] text-white">{{ heroBrandName }}</span>
|
||||
<span class="h-3.5 w-px shrink-0 bg-white/25" />
|
||||
<Transition name="hero-brand-message" mode="out-in">
|
||||
<span :key="`${locale}-${heroBrandMessageIndex}`" class="truncate text-white/90">{{ activeHeroBrandMessage }}</span>
|
||||
</Transition>
|
||||
</div>
|
||||
<div class="relative mt-6 h-px bg-white/20" />
|
||||
<p class="relative mt-4 text-xs leading-5 text-red-200/60">{{ t('home.cards.heroDesc') }}</p>
|
||||
<p class="relative mt-4 whitespace-pre-line text-xs leading-5 text-red-200/60">{{ t('home.cards.heroDesc') }}</p>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
@ -623,12 +591,17 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="home-slogan-row mt-5 flex items-center justify-center">
|
||||
<div class="flex w-full flex-wrap items-center justify-center gap-x-4 gap-y-1 px-4 py-2 text-center">
|
||||
<span class="text-lg font-semibold tracking-[0.18em] text-slate-900 sm:text-xl">{{ t('home.cards.heroFooterTitle') }}</span>
|
||||
<span class="text-slate-300">|</span>
|
||||
<span class="text-sm tracking-[0.08em] text-slate-500 sm:text-base">{{ t('home.cards.heroFooterSubTitle') }}</span>
|
||||
</div>
|
||||
<div class="home-disclaimer mt-5 text-center">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex cursor-pointer items-center justify-center rounded-2xl border border-slate-300/60 bg-white/55 px-5 py-2 text-base font-semibold text-slate-700 shadow-[0_8px_24px_rgba(15,23,42,0.06)] backdrop-blur-sm underline decoration-slate-300 underline-offset-4 transition hover:border-slate-400/70 hover:bg-white/70 hover:text-slate-900 hover:decoration-slate-500"
|
||||
@click="openDisclaimerPage"
|
||||
>
|
||||
免责声明:跳转到免责声明页面
|
||||
</button>
|
||||
<p class="mt-2 text-xs leading-5 text-slate-500">
|
||||
本计算工具由众为工程咨询有限公司提供免费技术支持
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -782,6 +755,13 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.home-title {
|
||||
animation: fade-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) 0.15s both;
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
padding: 18px 24px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.26);
|
||||
border-radius: 20px;
|
||||
background-image: radial-gradient(ellipse 80% 60% at 50% -10%, rgba(59, 130, 246, 0.06), transparent 70%);
|
||||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
.home-entry-item {
|
||||
animation: fade-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
@ -793,6 +773,9 @@ onBeforeUnmount(() => {
|
||||
.home-entry-item--2 { animation-delay: 0.3s; }
|
||||
.home-entry-item--3 { animation-delay: 0.4s; }
|
||||
.home-entry-item--4 { animation-delay: 0.5s; }
|
||||
.home-disclaimer {
|
||||
animation: fade-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) 0.6s both;
|
||||
}
|
||||
|
||||
@keyframes hero-in {
|
||||
from { opacity: 0; transform: translateX(-20px) scale(0.97); }
|
||||
@ -870,13 +853,4 @@ onBeforeUnmount(() => {
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.hero-brand-message-enter-active,
|
||||
.hero-brand-message-leave-active {
|
||||
transition: opacity 0.22s ease, transform 0.22s ease;
|
||||
}
|
||||
.hero-brand-message-enter-from,
|
||||
.hero-brand-message-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -53,6 +53,10 @@ const { t, locale } = useI18n()
|
||||
const DEFAULT_PROJECT_NAME = t('xmInfo.defaultProjectName')
|
||||
const DEFAULT_DESC = t('xmInfo.defaultDesc')
|
||||
const INDUSTRY_HINT_TEXT = computed(() => t('xmInfo.industryHint'))
|
||||
const REPORT_CONTENT_HINT_TEXT = computed(() => t('xmInfo.reportContentHint'))
|
||||
const OTHER_DESC_HINT_TEXT = computed(() => t('xmInfo.otherDescHint'))
|
||||
const FIELD_HINT_BUTTON_CLASS = 'inline-flex h-5 w-5 items-center justify-center text-muted-foreground/90 transition hover:text-foreground'
|
||||
const FIELD_HINT_ICON_CLASS = 'h-4 w-4'
|
||||
const getTodayDateString = () => {
|
||||
const now = new Date()
|
||||
const year = String(now.getFullYear())
|
||||
@ -231,7 +235,21 @@ onMounted(async () => {
|
||||
<div v-else class="rounded-xl border bg-card p-4 shadow-sm shrink-0 md:p-5">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="md:col-span-2 xl:col-span-4">
|
||||
<label class="block text-sm font-medium text-foreground">{{ t('xmInfo.fields.projectName') }}</label>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<label class="block text-sm font-medium text-foreground">{{ t('xmInfo.fields.projectName') }}</label>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="t('xmInfo.reportContentHintAria')"
|
||||
:class="FIELD_HINT_BUTTON_CLASS"
|
||||
>
|
||||
<CircleHelp :class="FIELD_HINT_ICON_CLASS" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">{{ REPORT_CONTENT_HINT_TEXT }}</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</div>
|
||||
<input
|
||||
v-model="projectName"
|
||||
type="text"
|
||||
@ -250,9 +268,9 @@ onMounted(async () => {
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="t('xmInfo.industryHintAria')"
|
||||
class="inline-flex h-5 w-5 items-center justify-center text-muted-foreground/90 transition hover:text-foreground"
|
||||
:class="FIELD_HINT_BUTTON_CLASS"
|
||||
>
|
||||
<CircleHelp class="h-5 w-5" />
|
||||
<CircleHelp :class="FIELD_HINT_ICON_CLASS" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">{{ INDUSTRY_HINT_TEXT }}</TooltipContent>
|
||||
@ -275,7 +293,7 @@ onMounted(async () => {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2 xl:col-span-4">
|
||||
<div class="md:col-span-2 xl:col-span-4">
|
||||
<label class="block text-sm font-medium text-foreground">{{ t('xmInfo.fields.overview') }}</label>
|
||||
<textarea
|
||||
v-model="overview"
|
||||
@ -427,10 +445,25 @@ onMounted(async () => {
|
||||
|
||||
|
||||
<div class="md:col-span-2 xl:col-span-4">
|
||||
<label class="block text-sm font-medium text-foreground">{{ t('xmInfo.fields.desc') }}</label>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<label class="block text-sm font-medium text-foreground">{{ t('xmInfo.fields.desc') }}</label>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="t('xmInfo.otherDescHintAria')"
|
||||
:class="FIELD_HINT_BUTTON_CLASS"
|
||||
>
|
||||
<CircleHelp :class="FIELD_HINT_ICON_CLASS" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">{{ OTHER_DESC_HINT_TEXT }}</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="desc"
|
||||
rows="4"
|
||||
:placeholder="t('xmInfo.placeholders.desc')"
|
||||
class="mt-2 w-full rounded-lg border bg-background px-4 py-2 text-sm outline-none ring-offset-background shadow-sm transition placeholder:text-muted-foreground/70 focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -26,14 +26,6 @@ export const enUS = {
|
||||
cards: {
|
||||
heroTitle: 'One-Click Smart Budget',
|
||||
heroSubTitle: 'Accelerate standards adoption',
|
||||
heroFooterTitle: 'Instant Cost Intelligence',
|
||||
heroFooterSubTitle: 'Leave your time for creation',
|
||||
heroBrand: 'Huizhongyi',
|
||||
heroBrandMessage1: 'So easy',
|
||||
heroBrandMessage2: 'Fee calc made easy',
|
||||
heroBrandMessage3: 'No late nights for fees',
|
||||
heroBrandMessage4: 'No heavy lifting for fees',
|
||||
heroBrandMessage5: 'No late nights, no heavy lifting',
|
||||
heroDesc: 'Cost consulting fee calculator for transport construction projects',
|
||||
projectBudget: 'Project Budget',
|
||||
projectBudgetDesc: 'For full project-level calculation across multiple contracts with import/export support',
|
||||
@ -307,7 +299,9 @@ export const enUS = {
|
||||
reserveTitle: 'Reserve Fee'
|
||||
},
|
||||
htInfo: {
|
||||
scaleDetailTitle: 'Contract Scale Details'
|
||||
scaleDetailTitle: 'Contract Scale Details',
|
||||
scaleDetailHint: 'When the scale data in this table differs from the project scale data, pricing will use this table.',
|
||||
scaleDetailHintAria: 'Contract scale details hint'
|
||||
},
|
||||
htFeeRate: {
|
||||
baseLabel: 'Base (total budget of all service fees)',
|
||||
@ -322,7 +316,7 @@ export const enUS = {
|
||||
title: 'Consulting Service Details',
|
||||
warning: 'Please review and adjust recommended limits/special values in the specification, then update final fee if needed.',
|
||||
editTabTitle: 'Service Edit-{name}',
|
||||
subtotal: 'Subtotal',
|
||||
subtotal: 'Total',
|
||||
edit: 'Edit',
|
||||
resetDefault: 'Reset',
|
||||
delete: 'Remove',
|
||||
@ -372,7 +366,7 @@ export const enUS = {
|
||||
}
|
||||
},
|
||||
htFeeGrid: {
|
||||
subtotal: 'Subtotal',
|
||||
subtotal: 'Total',
|
||||
currentRow: 'Current Row',
|
||||
unnamed: 'Unnamed',
|
||||
edit: 'Edit',
|
||||
@ -404,6 +398,8 @@ export const enUS = {
|
||||
},
|
||||
serviceSelector: {
|
||||
title: 'Select Services',
|
||||
titleHint: 'Some selectable services have been added beyond those listed in the specification. These additions do not conflict with the specification and are only included to support fee calculation under the specification.',
|
||||
titleHintAria: 'Select services hint',
|
||||
clear: 'Clear',
|
||||
empty: 'No services'
|
||||
},
|
||||
@ -439,7 +435,7 @@ export const enUS = {
|
||||
}
|
||||
},
|
||||
htFeeDetail: {
|
||||
subtotal: 'Subtotal',
|
||||
subtotal: 'Total',
|
||||
currentRow: 'Current Row',
|
||||
clickToInput: 'Click to input',
|
||||
addRow: 'Add Row',
|
||||
@ -600,7 +596,8 @@ export const enUS = {
|
||||
resetInvestAmount: 'Click ↻ to restore default cost amount for this column',
|
||||
resetLandArea: 'Click ↻ to restore default land area for this column',
|
||||
resetConsultCategoryFactor: 'Click ↻ to restore default consult category factor for this column',
|
||||
resetMajorFactor: 'Click ↻ to restore default major factor for this column'
|
||||
resetMajorFactor: 'Click ↻ to restore default major factor for this column',
|
||||
workRatio: 'This coefficient applies in two cases: service budget composition ratio, for cases where only part of the entrusted work in Appendix D Tables D.2 to D.7 of the specification is included; and quantity ratio, for cases where the calculation base uses the scale of each deliverable, batched task, single project, or unit project rather than the total amount, with quantity representing multiple items, copies, or units.'
|
||||
}
|
||||
},
|
||||
pricingPane: {
|
||||
@ -612,6 +609,8 @@ export const enUS = {
|
||||
confirmOverride: 'Confirm Override',
|
||||
investment: {
|
||||
title: 'Investment Scale Details',
|
||||
titleHint: 'Budget amount values in this pane follow whichever was operated later between the contract scale table and this pane.',
|
||||
titleHintAria: 'Investment scale details hint',
|
||||
clearDesc: 'This will clear current investment scale details. Continue?',
|
||||
overrideDesc: 'Use contract default data to override current investment scale details. Continue?'
|
||||
},
|
||||
@ -666,6 +665,10 @@ export const enUS = {
|
||||
defaultDesc: 'When providing cost consulting services, penalties should be graded by service quality. For scores >=85 and <90, penalty is 10% of budget fee; >=80 and <85: 20%; >=75 and <80: 30%; >=70 and <75: 40%; <70: 50% or above.',
|
||||
industryHint: 'Changing industry requires reset and re-selection',
|
||||
industryHintAria: 'Industry hint',
|
||||
reportContentHint: 'This field is optional and is only used to auto-generate report content.',
|
||||
reportContentHintAria: 'Report content hint',
|
||||
otherDescHint: 'This field is optional. The current content is only a sample. The preparer may fill it in according to actual needs or leave it blank.',
|
||||
otherDescHintAria: 'Other notes hint',
|
||||
createFromHomeFirst: 'Please create a project from Home before entering this page.',
|
||||
fields: {
|
||||
projectName: 'Project Name',
|
||||
@ -679,9 +682,10 @@ export const enUS = {
|
||||
},
|
||||
placeholders: {
|
||||
overview: 'Enter project overview',
|
||||
preparedBy: 'Enter preparer',
|
||||
reviewedBy: 'Enter reviewer',
|
||||
preparedCompany: 'Enter prepared company'
|
||||
desc: 'Other Notes',
|
||||
preparedBy: 'XXX',
|
||||
reviewedBy: 'XXX',
|
||||
preparedCompany: 'XXX'
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
@ -19,22 +19,14 @@ export const zhCN = {
|
||||
}
|
||||
},
|
||||
home: {
|
||||
title: '计算入口',
|
||||
title: '预算编制入口',
|
||||
subtitle: '项目计算 · 单项速算 · 导入数据',
|
||||
projectCalcTab: '项目计算',
|
||||
quickCalcTab: '快速计算',
|
||||
cards: {
|
||||
heroTitle: '智能预算一键生成',
|
||||
heroSubTitle: '助力《规范》高效落地',
|
||||
heroFooterTitle: '智算费用 即点即出',
|
||||
heroFooterSubTitle: '您的时间留给创造',
|
||||
heroBrand: '慧众易',
|
||||
heroBrandMessage1: '真容易',
|
||||
heroBrandMessage2: '算费真容易',
|
||||
heroBrandMessage3: '算费不熬夜',
|
||||
heroBrandMessage4: '算费不费力',
|
||||
heroBrandMessage5: '不熬夜,不费力',
|
||||
heroDesc: '交通建设项目工程造价咨询服务费计算',
|
||||
heroDesc: '《交通运输工程造价咨询服务预算编制规范》\n(T/GDHS 017-2026)预算编制工具',
|
||||
projectBudget: '项目预算',
|
||||
projectBudgetDesc: '适用于多合同段、项目级整体计算,支持导出/导入完整项目数据',
|
||||
quickCalc: '单项速算',
|
||||
@ -198,7 +190,7 @@ export const zhCN = {
|
||||
copied: '已复制',
|
||||
copyFailed: '复制失败',
|
||||
brandAlt: '众为咨询',
|
||||
supportText: '本网站由众为工程咨询有限公司提供免费技术支持',
|
||||
supportText: '本计算工具由众为工程咨询有限公司提供免费技术支持',
|
||||
aboutTitle: '关于我们',
|
||||
companyName: '众为工程咨询有限公司',
|
||||
openOfficialSiteAria: '跳转到官网首页',
|
||||
@ -307,7 +299,9 @@ export const zhCN = {
|
||||
reserveTitle: '预备费'
|
||||
},
|
||||
htInfo: {
|
||||
scaleDetailTitle: '合同规模明细'
|
||||
scaleDetailTitle: '合同规模明细',
|
||||
scaleDetailHint: '当本表规模与项目规模数据不一致时,计费以本表规模为准',
|
||||
scaleDetailHintAria: '合同规模明细提示'
|
||||
},
|
||||
htFeeRate: {
|
||||
baseLabel: '基数(所有服务费预算合计)',
|
||||
@ -322,7 +316,7 @@ export const zhCN = {
|
||||
title: '咨询服务明细',
|
||||
warning: '※ 请注意检查并修改《规范》建议的限值或特殊值,并在确认金额栏修改',
|
||||
editTabTitle: '服务编辑-{name}',
|
||||
subtotal: '小计',
|
||||
subtotal: '总计',
|
||||
edit: '编辑',
|
||||
resetDefault: '恢复默认',
|
||||
delete: '删除',
|
||||
@ -352,7 +346,7 @@ export const zhCN = {
|
||||
},
|
||||
htSummary: {
|
||||
title: '合同段汇总',
|
||||
total: '合计',
|
||||
total: '总计',
|
||||
remark: '说明',
|
||||
placeholder: '请先填咨询服务/附加工作费/预备费的数据',
|
||||
additionalPrefix: '附加工作费',
|
||||
@ -372,7 +366,7 @@ export const zhCN = {
|
||||
}
|
||||
},
|
||||
htFeeGrid: {
|
||||
subtotal: '小计',
|
||||
subtotal: '总计',
|
||||
currentRow: '当前行',
|
||||
unnamed: '未命名',
|
||||
edit: '编辑',
|
||||
@ -404,6 +398,8 @@ export const zhCN = {
|
||||
},
|
||||
serviceSelector: {
|
||||
title: '选择服务',
|
||||
titleHint: '本选择项较《规范》列明的服务项有所增加。此增加与《规范》无冲突,只为满足《规范》费用计算之需',
|
||||
titleHintAria: '选择服务提示',
|
||||
clear: '清空',
|
||||
empty: '暂无服务'
|
||||
},
|
||||
@ -439,7 +435,7 @@ export const zhCN = {
|
||||
}
|
||||
},
|
||||
htFeeDetail: {
|
||||
subtotal: '小计',
|
||||
subtotal: '总计',
|
||||
currentRow: '当前行',
|
||||
clickToInput: '点击输入',
|
||||
addRow: '添加行',
|
||||
@ -596,10 +592,11 @@ export const zhCN = {
|
||||
majorGroup: '专业编码以及工程专业名称'
|
||||
},
|
||||
tooltip: {
|
||||
resetInvestAmount: '点击右侧↻恢复本列默认造价金额',
|
||||
resetLandArea: '点击右侧↻恢复本列默认用地面积',
|
||||
resetConsultCategoryFactor: '点击右侧↻恢复本列默认咨询分类系数',
|
||||
resetMajorFactor: '点击右侧↻恢复本列默认专业系数'
|
||||
resetInvestAmount: '点击右侧↻本列造价金额数值恢复为本合同规模数值',
|
||||
resetLandArea: '点击右侧↻本列用地面积数值恢复为本合同规模数值',
|
||||
resetConsultCategoryFactor: '点击右侧↻本列咨询分类系数值恢复为本项目的相应的咨询分类系数值',
|
||||
resetMajorFactor: '点击右侧↻本列专业系数值恢复为本项目的相应的专业系数值',
|
||||
workRatio: '本列系数适用于以下两种情形:服务预算构成比率:适用于《规范》附录D表D.2~D.7中委托工作内容仅为部分工作时的情形;数量比,适用于计算基数按每份成果、分批次任务、单项工程或单位工程的规模(非总额)的情形,数量表示有多个个、份或项'
|
||||
}
|
||||
},
|
||||
pricingPane: {
|
||||
@ -611,6 +608,8 @@ export const zhCN = {
|
||||
confirmOverride: '确认覆盖',
|
||||
investment: {
|
||||
title: '投资规模明细',
|
||||
titleHint: '本表造价金额取值规则:以本合同规模表与本表造价金额中操作时间较晚的值为准',
|
||||
titleHintAria: '投资规模明细提示',
|
||||
clearDesc: '将清空当前投资规模明细,是否继续?',
|
||||
overrideDesc: '将使用合同默认数据覆盖当前投资规模明细,是否继续?'
|
||||
},
|
||||
@ -626,7 +625,7 @@ export const zhCN = {
|
||||
unavailableMessage: '当前服务没有关联工作量法任务,无需填写此部分内容。',
|
||||
clickToInput: '点击输入',
|
||||
none: '无',
|
||||
total: '总合计',
|
||||
total: '总计',
|
||||
columns: {
|
||||
code: '编码',
|
||||
name: '名称',
|
||||
@ -642,7 +641,7 @@ export const zhCN = {
|
||||
hourlyFeeGrid: {
|
||||
title: '工时法明细',
|
||||
clickToInput: '点击输入',
|
||||
total: '总合计',
|
||||
total: '总计',
|
||||
columns: {
|
||||
code: '编码',
|
||||
name: '人员名称',
|
||||
@ -665,6 +664,10 @@ export const zhCN = {
|
||||
defaultDesc: '在履行造价咨询服务时,宜根据咨询服务质量情况分级确定相应的处罚金额。其中考评得分在大于及等于85和小于90分时,处罚金额为预算费用的10%;其中考评得分在大于及等于80和小于85分时,处罚金额为预算费用的20%;其中考评得分在大于及等于75和小于80分时,处罚金额为预算费用的30%;其中考评得分在大于及等于70和小于75分时,处罚金额为预算费用的40%;其中考评得分小于70分时,处罚金额为预算费用的50%以上。',
|
||||
industryHint: '变更需要重置后重新选择',
|
||||
industryHintAria: '工程行业提示',
|
||||
reportContentHint: '本内容为选择性填写,填写内容仅用于自动生成编制报告内容',
|
||||
reportContentHintAria: '说明内容提示',
|
||||
otherDescHint: '本内容为选择性填写。当前显示内容仅为示意,编制人可根据实际情况填写,亦可不填。',
|
||||
otherDescHintAria: '其他说明提示',
|
||||
createFromHomeFirst: '请从首页先新建项目后再进入此页面。',
|
||||
fields: {
|
||||
projectName: '项目名称',
|
||||
@ -678,9 +681,10 @@ export const zhCN = {
|
||||
},
|
||||
placeholders: {
|
||||
overview: '请输入项目概况',
|
||||
preparedBy: '请输入编制人',
|
||||
reviewedBy: '请输入复核人',
|
||||
preparedCompany: '请输入编制单位'
|
||||
desc: '其他说明',
|
||||
preparedBy: 'XXX',
|
||||
reviewedBy: 'XXX',
|
||||
preparedCompany: 'XXX'
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
@ -560,8 +560,13 @@ const handleCreateProjectConfirm = () => {
|
||||
projectMenuOpen.value = false
|
||||
}
|
||||
|
||||
const returnToHome = () => {
|
||||
const returnToHome = async () => {
|
||||
projectMenuOpen.value = false
|
||||
if (readWorkspaceMode() === 'quick') {
|
||||
tabStore.resetTabs()
|
||||
writeWorkspaceMode('project')
|
||||
await tabStore.$persistNow?.()
|
||||
}
|
||||
const href = buildProjectUrl(currentProjectId.value || readCurrentProjectId(), {
|
||||
forceHome: true
|
||||
})
|
||||
|
||||
@ -69,8 +69,38 @@ export class AgGridResetHeader implements IHeaderComp {
|
||||
this.params = params
|
||||
this.eLabel.textContent = params.displayName || params.column?.getColDef().headerName || ''
|
||||
const fallbackResetTitle = i18n.global.t('agGrid.resetDefault')
|
||||
const hintText = String(params.column?.getColDef().headerTooltip || '').trim()
|
||||
const hasReset = Boolean(params.onReset)
|
||||
const hasHint = Boolean(hintText)
|
||||
|
||||
if (hasReset) {
|
||||
this.eButton.textContent = '↻'
|
||||
this.eButton.title = ''
|
||||
this.eButton.setAttribute('aria-label', params.resetTitle || fallbackResetTitle)
|
||||
this.eButton.style.visibility = 'visible'
|
||||
this.eButton.style.border = '1px solid #d1d5db'
|
||||
this.eButton.style.background = '#edff87'
|
||||
this.eButton.style.color = '#4b5563'
|
||||
this.eButton.style.cursor = 'pointer'
|
||||
return true
|
||||
}
|
||||
|
||||
if (hasHint) {
|
||||
this.eButton.textContent = '?'
|
||||
this.eButton.title = hintText
|
||||
this.eButton.setAttribute('aria-label', hintText)
|
||||
this.eButton.style.visibility = 'visible'
|
||||
this.eButton.style.border = '1px solid #cbd5e1'
|
||||
this.eButton.style.background = '#ffffff'
|
||||
this.eButton.style.color = '#64748b'
|
||||
this.eButton.style.cursor = 'help'
|
||||
return true
|
||||
}
|
||||
|
||||
this.eButton.textContent = ''
|
||||
this.eButton.title = ''
|
||||
this.eButton.setAttribute('aria-label', params.resetTitle || fallbackResetTitle)
|
||||
this.eButton.style.visibility = params.onReset ? 'visible' : 'hidden'
|
||||
this.eButton.style.visibility = 'hidden'
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
formatScaleReadonlyMoney,
|
||||
getScaleMergeColSpanBeforeTotal
|
||||
} from '@/lib/pricingScaleGrid'
|
||||
import { AgGridResetHeader } from '@/lib/agGridResetHeader'
|
||||
import { i18n } from '@/i18n'
|
||||
|
||||
type ScaleColumnField<TRow> = Extract<keyof TRow, string> | string
|
||||
@ -199,6 +200,8 @@ export const createScaleBudgetFeeColumnGroup = <TRow>(options: {
|
||||
headerName: scaleT('columns.workRatio'),
|
||||
field: 'workRatio' as any,
|
||||
colId: 'workRatio',
|
||||
headerTooltip: scaleT('tooltip.workRatio'),
|
||||
headerComponent: AgGridResetHeader,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
@ -263,6 +266,9 @@ export const createScaleAutoGroupColumn = <TRow>(options: {
|
||||
flex: 2,
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
cellClassRules: {
|
||||
'ag-summary-label-cell': params => Boolean(params.node?.rowPinned)
|
||||
},
|
||||
cellStyle: {
|
||||
whiteSpace: 'normal',
|
||||
lineHeight: '1.4'
|
||||
|
||||
@ -186,6 +186,23 @@ input[inputmode='numeric'] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ag-theme-quartz .ag-cell.ag-summary-label-cell {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ag-theme-quartz .ag-cell.ag-summary-label-cell .ag-cell-wrapper {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ag-theme-quartz .ag-cell.ag-summary-label-cell .ag-cell-value {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Global AG Grid header alignment: center all header text. */
|
||||
.ag-theme-quartz .ag-header-cell-label,
|
||||
.ag-theme-quartz .ag-header-group-cell-label {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user