From f2138487191ad51da486abe29e685a5f48546b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=84=A6=E9=BE=99=E8=A8=80?= Date: Tue, 23 Jun 2026 09:13:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0UI=E7=9B=B8=E5=85=B3=E9=83=A8?= =?UTF-8?q?=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 16 + .env | 9 + .env.docker.example | 9 + Dockerfile | 21 + README.md | 38 + docker-compose.yml | 46 + frontend/Dockerfile | 18 + frontend/src/assets/styles.css | 960 +++++++++++++++--- frontend/src/components/StatCard.vue | 16 +- frontend/src/stores/theme.ts | 64 +- frontend/src/views/CommissionsView.vue | 2 +- frontend/src/views/ConfigCenterView.vue | 2 +- frontend/src/views/DashboardView.vue | 2 +- frontend/src/views/OperationLogsView.vue | 2 +- frontend/src/views/PayrollView.vue | 2 +- frontend/src/views/ReportsView.vue | 170 +++- frontend/src/views/SalaryProfilesView.vue | 349 ++++--- frontend/src/views/UsersView.vue | 2 +- frontend/tests/cardSystemConsistency.test.ts | 51 + frontend/tests/compactSummaryStrip.test.ts | 69 ++ frontend/tests/globalUiAtmosphere.test.ts | 96 ++ frontend/tests/reportsViewLayout.test.ts | 87 ++ .../tests/salaryProfileAtmosphere.test.ts | 71 ++ .../tests/salaryProfileCardsLayout.test.ts | 50 + frontend/tests/toastMessagesCss.test.ts | 46 + frontend/tests/visualPolishCss.test.ts | 7 + tests/payroll_exceptions_test.py | 55 + tests/realtime_attendance_service_test.py | 80 ++ tests/settings_env_test.py | 47 + 29 files changed, 1990 insertions(+), 397 deletions(-) create mode 100644 .dockerignore create mode 100644 .env create mode 100644 .env.docker.example create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 frontend/Dockerfile create mode 100644 frontend/tests/cardSystemConsistency.test.ts create mode 100644 frontend/tests/compactSummaryStrip.test.ts create mode 100644 frontend/tests/globalUiAtmosphere.test.ts create mode 100644 frontend/tests/reportsViewLayout.test.ts create mode 100644 frontend/tests/salaryProfileAtmosphere.test.ts create mode 100644 frontend/tests/salaryProfileCardsLayout.test.ts create mode 100644 frontend/tests/toastMessagesCss.test.ts create mode 100644 tests/payroll_exceptions_test.py create mode 100644 tests/realtime_attendance_service_test.py create mode 100644 tests/settings_env_test.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dabca2b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git/ +.idea/ +.venv/ +__pycache__/ +*.py[cod] +.env +.env.* +!.env.example +!.env.docker.example +data/ +logs/ +outputs/ +uploads/ +static/uploads/ +frontend/node_modules/ +frontend/dist/ diff --git a/.env b/.env new file mode 100644 index 0000000..71bb558 --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +# Copy this file to .env and edit DATABASE_URL before running docker compose. +# +# If MySQL runs on the same host as Docker, use host.docker.internal. +# docker-compose.yml maps it to the Linux Docker host with host-gateway. +# If MySQL runs on another server, use that server's reachable IP address or domain name. +DATABASE_URL=mysql+pymysql://root:Plo6lvzOPtMNzVIA@host.docker.internal:3306/financial_system?charset=utf8mb4 + +BACKEND_PORT=8000 +FRONTEND_PORT=5173 diff --git a/.env.docker.example b/.env.docker.example new file mode 100644 index 0000000..0c9fa19 --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,9 @@ +# Copy this file to .env and edit DATABASE_URL before running docker compose. +# +# If MySQL runs on the same host as Docker, use host.docker.internal. +# docker-compose.yml maps it to the Linux Docker host with host-gateway. +# If MySQL runs on another server, use that server's reachable IP address or domain name. +DATABASE_URL=mysql+pymysql://root:12345678@host.docker.internal:3306/financial_system?charset=utf8mb4 + +BACKEND_PORT=8000 +FRONTEND_PORT=5173 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1450702 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV APP_CONFIG_FILE=/app/config/app_settings.json + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY main.py . +COPY config ./config +COPY financial_system ./financial_system +COPY static ./static + +RUN mkdir -p logs uploads outputs static/uploads/avatars + +EXPOSE 8000 + +CMD ["python", "main.py"] diff --git a/README.md b/README.md index 61f6c44..0d8ccf9 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,44 @@ cd frontend npm run build ``` +## Docker 运行(外部 MySQL) + +项目已提供后端镜像、前端 Nginx 镜像和 `docker-compose.yml`。数据库使用外部 MySQL,不会在 compose 里启动 MySQL 容器。 + +1. 准备环境变量: + +```bash +cp .env.docker.example .env +``` + +2. 修改 `.env` 里的 `DATABASE_URL`: + +```env +DATABASE_URL=mysql+pymysql://用户名:密码@MySQL地址:3306/financial_system?charset=utf8mb4 +``` + +如果 MySQL 就运行在当前电脑,并且你使用 Docker Desktop,MySQL 地址通常写: + +```env +DATABASE_URL=mysql+pymysql://root:12345678@host.docker.internal:3306/financial_system?charset=utf8mb4 +``` + +如果 MySQL 在另一台服务器,写那台服务器的内网 IP、外网 IP 或域名。不要在容器里使用 `localhost` 连接外部 MySQL,因为 `localhost` 指向的是后端容器本身。 + +3. 启动: + +```bash +docker compose up --build +``` + +默认访问: + +- 前端系统:http://127.0.0.1:5173 +- 后端接口:http://127.0.0.1:8000 +- 接口文档:http://127.0.0.1:5173/docs + +后端容器启动时会按外部 MySQL 自动建库、建表并初始化默认超级管理员。请确认外部 MySQL 已允许该账号远程连接,并且账号有创建数据库和建表权限。 + ## 统一配置 后端配置统一放在: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f4d9c39 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,46 @@ +services: + backend: + build: + context: . + dockerfile: Dockerfile + container_name: financial-system-backend + environment: + APP_CONFIG_FILE: /app/config/app_settings.json + DATABASE_URL: ${DATABASE_URL:?Please set DATABASE_URL in .env} + extra_hosts: + - "host.docker.internal:host-gateway" + ports: + - "${BACKEND_PORT:-8000}:8000" + volumes: + - ./config/app_settings.json:/app/config/app_settings.json:ro + - ./uploads:/app/uploads + - ./outputs:/app/outputs + - ./logs:/app/logs + - ./static/uploads:/app/static/uploads + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)", + ] + interval: 15s + timeout: 5s + retries: 10 + start_period: 20s + restart: unless-stopped + + frontend: + build: + context: . + dockerfile: frontend/Dockerfile + args: + VITE_API_BASE_URL: "" + container_name: financial-system-frontend + depends_on: + backend: + condition: service_started + ports: + - "${FRONTEND_PORT:-5173}:80" + restart: unless-stopped diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..71768e7 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20-alpine AS build + +WORKDIR /app + +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend/ ./ +ARG VITE_API_BASE_URL= +ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} +RUN npm run build + +FROM nginx:1.27-alpine + +COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/frontend/src/assets/styles.css b/frontend/src/assets/styles.css index 446746c..f9448cf 100644 --- a/frontend/src/assets/styles.css +++ b/frontend/src/assets/styles.css @@ -16,6 +16,11 @@ --red: #bd1e1e; --red-soft: #ffebe8; --shadow: 0 16px 32px rgba(4, 22, 39, 0.06); + --card-border: 1px solid var(--line); + --card-shadow: 0 6px 16px rgba(var(--primary-rgb), 0.035); + --card-inner-border: 1px solid var(--line-soft); + --card-inner-bg: var(--surface); + --card-inner-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.56); --sidebar-bg: #ffffff; --nav-text: #334155; --nav-active-bg: #e8f0ff; @@ -36,15 +41,15 @@ --radius-sm: 5px; --radius-md: 7px; --radius-lg: 10px; - --topbar-height: 64px; - --nav-height: 42px; - --control-height: 40px; - --base-font-size: 15px; - --page-padding: 24px; - --content-gap: 18px; - --panel-padding: 20px; - --table-cell-y: 12px; - --table-cell-x: 18px; + --topbar-height: 60px; + --nav-height: 38px; + --control-height: 38px; + --base-font-size: 14px; + --page-padding: 20px; + --content-gap: 14px; + --panel-padding: 16px; + --table-cell-y: 10px; + --table-cell-x: 14px; --layout-sidebar-width: 280px; --layout-sidebar-offset: 280px; --collapsed-sidebar-width: 88px; @@ -197,8 +202,8 @@ a { .brand { display: flex; align-items: center; - gap: 12px; - padding: calc(var(--panel-padding) + 8px) var(--panel-padding) var(--panel-padding); + gap: 10px; + padding: calc(var(--panel-padding) + 6px) var(--panel-padding) calc(var(--panel-padding) - 2px); transition: gap var(--motion-duration) var(--motion-easing), padding var(--motion-duration) var(--motion-easing); @@ -207,7 +212,7 @@ a { .brand-main { display: flex; align-items: center; - gap: 12px; + gap: 10px; min-width: 0; } @@ -226,8 +231,8 @@ a { .brand-mark { display: grid; - width: 40px; - height: 40px; + width: 36px; + height: 36px; place-items: center; flex: 0 0 auto; border-radius: var(--radius-md); @@ -249,7 +254,7 @@ a { .brand-title { margin: 0; color: var(--primary); - font-size: 18px; + font-size: 16px; font-weight: 800; } @@ -263,10 +268,10 @@ a { display: grid; flex: 1; align-content: start; - gap: 8px; + gap: 6px; min-height: 0; overflow-y: auto; - padding: 16px; + padding: 12px; transition: gap var(--motion-duration) var(--motion-easing), padding var(--motion-duration) var(--motion-easing); @@ -323,12 +328,12 @@ a { .nav-section { display: grid; - gap: 5px; + gap: 4px; } .nav-section + .nav-section { - margin-top: 8px; - padding-top: 8px; + margin-top: 6px; + padding-top: 6px; border-top: 1px solid var(--line-soft); } @@ -353,14 +358,14 @@ a { .nav-link { display: flex; align-items: center; - gap: 12px; + gap: 10px; min-height: var(--nav-height); width: 100%; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--nav-text); - padding: 0 14px; + padding: 0 12px; text-align: left; transition: background var(--motion-duration-fast) ease, @@ -403,9 +408,9 @@ a { .sidebar-footer { display: grid; flex: 0 0 auto; - gap: 6px; + gap: 5px; border-top: 1px solid var(--line); - padding: 16px; + padding: 12px; transition: gap var(--motion-duration) var(--motion-easing), padding var(--motion-duration) var(--motion-easing); @@ -419,7 +424,7 @@ a { min-height: var(--topbar-height); align-items: center; justify-content: space-between; - gap: var(--content-gap); + gap: 12px; border-bottom: 1px solid var(--line-soft); background: var(--topbar-bg); padding: 0 var(--page-padding); @@ -430,13 +435,13 @@ a { .topbar-actions { display: flex; align-items: center; - gap: 16px; + gap: 12px; } .global-search { display: flex; - width: min(520px, 40vw); - min-width: 260px; + width: min(460px, 38vw); + min-width: 240px; align-items: center; gap: 10px; border: 1px solid var(--line); @@ -458,8 +463,8 @@ a { .icon-button { position: relative; display: grid; - width: 42px; - height: 42px; + width: 38px; + height: 38px; place-items: center; border: 0; border-radius: 999px; @@ -490,9 +495,9 @@ a { .user-summary { display: flex; align-items: center; - gap: 12px; + gap: 10px; border-left: 1px solid var(--line); - padding-left: 20px; + padding-left: 16px; } .profile-trigger { @@ -526,8 +531,8 @@ a { .avatar { display: grid; - width: 42px; - height: 42px; + width: 38px; + height: 38px; place-items: center; border: 2px solid var(--line); border-radius: 999px; @@ -744,9 +749,10 @@ a { display: grid; gap: 6px; min-width: 0; - border: 1px solid var(--line); + border: var(--card-inner-border); border-radius: var(--radius-md); - background: var(--surface); + background: var(--card-inner-bg); + box-shadow: var(--card-inner-shadow); color: var(--text); padding: 6px; text-align: center; @@ -929,8 +935,6 @@ a { .color-preset-choice { min-height: 74px; align-content: start; - border-color: var(--line); - background: var(--card); padding: 6px; } @@ -1292,8 +1296,9 @@ a { align-items: start; justify-items: center; background: var(--modal-backdrop); + backdrop-filter: blur(8px); overflow-y: auto; - padding: 48px 24px; + padding: 36px 20px; } .profile-modal { @@ -1303,7 +1308,7 @@ a { border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--card); - box-shadow: 0 26px 64px rgba(var(--primary-rgb), 0.22); + box-shadow: 0 22px 54px rgba(var(--primary-rgb), 0.2); } .employee-modal { @@ -1315,15 +1320,15 @@ a { display: flex; align-items: flex-start; justify-content: space-between; - gap: 18px; + gap: 14px; border-bottom: 1px solid var(--line-soft); - padding: 22px 24px; + padding: 18px var(--panel-padding); } .modal-header h2 { margin: 0; color: var(--primary); - font-size: 22px; + font-size: 20px; } .modal-header p, @@ -1335,8 +1340,8 @@ a { .profile-avatar-row { display: flex; align-items: center; - gap: 18px; - padding: 24px; + gap: 14px; + padding: var(--panel-padding); } .profile-avatar-row strong { @@ -1360,7 +1365,7 @@ a { .profile-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 18px; + gap: 12px; border-top: 1px solid var(--line-soft); padding: var(--panel-padding); } @@ -1372,7 +1377,7 @@ a { .password-form { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 18px; + gap: 12px; border-top: 1px solid var(--line-soft); padding: var(--panel-padding); } @@ -1405,29 +1410,31 @@ a { display: flex; align-items: flex-end; justify-content: space-between; - gap: 20px; + gap: 16px; + min-height: 52px; + padding-bottom: 2px; } .page-header h1 { margin: 0; color: var(--primary); - font-size: 32px; + font-size: 26px; line-height: 1.2; } .page-header p, .panel-header p, .action-card p { - margin: 8px 0 0; + margin: 5px 0 0; color: var(--muted); - line-height: 1.6; + line-height: 1.45; } .header-actions, .form-actions { display: flex; align-items: center; - gap: 12px; + gap: 10px; flex-wrap: wrap; } @@ -1443,10 +1450,10 @@ a { min-height: var(--control-height); align-items: center; justify-content: center; - gap: 9px; + gap: 8px; border-radius: var(--radius-md); border: 1px solid transparent; - padding: 0 18px; + padding: 0 14px; font-weight: 700; transition: background 0.18s ease, @@ -1458,7 +1465,7 @@ a { .primary-button { background: var(--blue); color: white; - box-shadow: 0 12px 22px rgba(var(--accent-rgb), 0.16); + box-shadow: 0 8px 18px rgba(var(--accent-rgb), 0.14); } .primary-button:hover { @@ -1466,6 +1473,11 @@ a { filter: brightness(0.94); } +.primary-button:focus-visible { + outline: 0; + box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.16); +} + .secondary-button { border-color: var(--line); background: var(--secondary-bg); @@ -1477,6 +1489,13 @@ a { background: var(--secondary-hover-bg); } +.secondary-button:focus-visible, +.link-button:focus-visible, +.file-button:focus-visible { + outline: 0; + box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.12); +} + .link-button { min-height: auto; border: 0; @@ -1490,77 +1509,117 @@ a { width: 100%; } -.stats-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: var(--content-gap); -} - -.stat-card, .panel, -.action-card { - border: 1px solid var(--line); +.action-card, +.salary-profile-summary-strip, +.organization-summary-strip { + border: var(--card-border); border-radius: var(--radius-lg); background: var(--card); - box-shadow: 0 8px 20px rgba(var(--primary-rgb), 0.04); + box-shadow: var(--card-shadow); } -.stat-card { - display: grid; - gap: 12px; - min-height: 178px; - padding: var(--panel-padding); -} - -.stat-head { +.summary-strip { display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 14px; + flex-wrap: wrap; + align-items: center; + gap: 8px; + border: var(--card-border); + border-radius: var(--radius-lg); + background: var(--card); + box-shadow: var(--card-shadow); + padding: 6px 8px; } -.stat-icon, +.summary-metric { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + min-height: 42px; + min-width: 180px; + flex: 1 1 220px; + align-items: center; + gap: 7px; + border: var(--card-inner-border); + border-radius: var(--radius-md); + background: var(--card-inner-bg); + box-shadow: var(--card-inner-shadow); + padding: 5px 8px; +} + +.summary-icon, .action-icon { display: grid; place-items: center; border-radius: var(--radius-md); } -.stat-icon { - width: 52px; - height: 52px; +.summary-icon { + width: 26px; + height: 26px; background: var(--surface-soft); color: var(--primary); } -.tone-blue .stat-icon { +.tone-blue .summary-icon { background: var(--blue-soft); color: var(--blue); } -.tone-green .stat-icon { +.tone-green .summary-icon { background: var(--green-soft); color: var(--green); } -.tone-red .stat-icon { +.tone-red .summary-icon { background: var(--red-soft); color: var(--red); } -.stat-meta, +.summary-copy { + display: grid; + min-width: 0; + gap: 2px; +} + +.summary-copy span { + overflow: hidden; + color: var(--muted); + font-size: 12px; + font-weight: 800; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.summary-copy strong { + overflow: hidden; + color: var(--primary); + font-size: 16px; + line-height: 1.15; + text-overflow: ellipsis; + white-space: nowrap; +} + +.summary-meta, .status-chip { display: inline-flex; align-items: center; - min-height: 26px; + min-height: 22px; border-radius: var(--radius-sm); - padding: 0 10px; + padding: 0 7px; font-size: 12px; font-weight: 800; white-space: nowrap; } -.stat-meta, +.summary-meta { + max-width: 132px; + overflow: hidden; + font-style: normal; + text-overflow: ellipsis; +} + +.summary-meta, .status-chip.neutral { background: var(--surface-soft); color: var(--muted); @@ -1576,19 +1635,6 @@ a { color: var(--red); } -.stat-card p { - margin: 0; - color: var(--muted); - font-size: 13px; - font-weight: 800; -} - -.stat-card strong { - color: var(--primary); - font-size: 28px; - line-height: 1.15; -} - .employee-header-actions { display: flex; align-items: center; @@ -1598,20 +1644,21 @@ a { .employee-inline-metric { display: flex; - min-height: 48px; + min-height: 42px; align-items: center; - gap: 10px; - border: 1px solid var(--line); + gap: 8px; + border: var(--card-inner-border); border-radius: var(--radius-md); - background: rgba(255, 255, 255, 0.58); + background: var(--card-inner-bg); + box-shadow: var(--card-inner-shadow); color: var(--primary); - padding: 8px 12px; + padding: 6px 10px; } .employee-inline-icon { display: grid; - width: 34px; - height: 34px; + width: 30px; + height: 30px; place-items: center; border-radius: var(--radius-sm); background: var(--blue-soft); @@ -1631,7 +1678,7 @@ a { .employee-inline-metric strong { color: var(--primary); - font-size: 21px; + font-size: 18px; line-height: 1; } @@ -1639,7 +1686,7 @@ a { border-radius: var(--radius-sm); background: var(--surface-soft); color: var(--muted); - padding: 4px 8px; + padding: 3px 7px; font-size: 12px; font-style: normal; font-weight: 800; @@ -1665,28 +1712,28 @@ a { display: flex; align-items: center; justify-content: space-between; - gap: 16px; + gap: 12px; border-bottom: 1px solid var(--line-soft); - padding: calc(var(--panel-padding) - 2px) var(--panel-padding); + padding: 14px var(--panel-padding); } .panel-header h2, .action-card h2 { margin: 0; color: var(--primary); - font-size: 21px; + font-size: 18px; line-height: 1.3; } .sync-panel, .trend-panel { - min-height: 360px; + min-height: 300px; } .compact-list, .detail-grid { display: grid; - gap: 18px; + gap: 12px; margin: 0; padding: var(--panel-padding); } @@ -1696,9 +1743,9 @@ a { display: flex; align-items: center; justify-content: space-between; - gap: 18px; + gap: 14px; border-bottom: 1px solid var(--line-soft); - padding-bottom: 16px; + padding-bottom: 12px; } .compact-list dt, @@ -1716,8 +1763,8 @@ a { } .sync-panel .primary-button { - margin: 0 24px 24px; - width: calc(100% - 48px); + margin: 0 var(--panel-padding) var(--panel-padding); + width: calc(100% - (var(--panel-padding) * 2)); } .legend-line { @@ -1747,11 +1794,11 @@ a { .bar-chart { display: grid; - height: 250px; + height: 210px; grid-template-columns: repeat(6, minmax(36px, 1fr)); align-items: end; - gap: var(--content-gap); - padding: 24px 34px 28px; + gap: 12px; + padding: 20px 28px 22px; } .bar-item { @@ -1766,7 +1813,7 @@ a { .bar-item strong, .bonus-bar { display: block; - width: 50px; + width: 42px; border-radius: var(--radius-sm) var(--radius-sm) 0 0; } @@ -1791,14 +1838,14 @@ a { position: relative; display: grid; align-content: start; - gap: 18px; - min-height: 260px; - padding: calc(var(--panel-padding) + 8px); + gap: 14px; + min-height: 210px; + padding: calc(var(--panel-padding) + 4px); } .action-icon { - width: 62px; - height: 62px; + width: 50px; + height: 50px; } .action-icon.blue { @@ -1827,7 +1874,7 @@ a { .form-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 18px; + gap: 12px; padding: var(--panel-padding); } @@ -1846,16 +1893,34 @@ a { .compact-stats-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); - gap: 10px; + gap: 8px; padding: 0 var(--panel-padding) var(--panel-padding); } +.summary-metric, +.compact-stat, +.salary-profile-summary-item, +.organization-summary-strip div, +.organization-node-summary div, +.employee-inline-metric, +.workflow-steps span, +.recent-item, +.salary-profile-modal-section, +.report-total-item, +.report-inline-metrics span, +.report-attendance-card { + border: var(--card-inner-border); + background: var(--card-inner-bg); + box-shadow: var(--card-inner-shadow); +} + .compact-stat { min-width: 0; - border: 1px solid var(--line); + border: var(--card-inner-border); border-radius: var(--radius-md); - background: var(--surface); - padding: 12px 14px; + background: var(--card-inner-bg); + box-shadow: var(--card-inner-shadow); + padding: 10px 12px; } .compact-stat span, @@ -1866,20 +1931,441 @@ a { .compact-stat strong { display: block; - margin-top: 6px; + margin-top: 5px; + color: var(--primary); + font-size: 18px; + line-height: 1.2; +} + +.salary-profile-page { + gap: 14px; +} + +.salary-profile-summary-strip { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + padding: 10px; +} + +.salary-profile-summary-item { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + grid-template-areas: + "icon label" + "icon value" + "icon meta"; + align-items: center; + column-gap: 10px; + min-width: 0; + border: var(--card-inner-border); + border-radius: var(--radius-md); + background: var(--card-inner-bg); + box-shadow: var(--card-inner-shadow); + padding: 10px 12px; +} + +.salary-profile-summary-item svg { + grid-area: icon; + color: var(--blue); +} + +.salary-profile-summary-item span, +.salary-profile-summary-item small { + overflow: hidden; + color: var(--muted); + font-size: 12px; + font-weight: 800; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.salary-profile-summary-item span { + grid-area: label; +} + +.salary-profile-summary-item strong { + grid-area: value; + min-width: 0; + overflow: hidden; color: var(--primary); font-size: 19px; line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.salary-profile-summary-item small { + grid-area: meta; +} + +.salary-profile-list-panel .empty-state { + min-height: 180px; +} + +.salary-profile-list-header { + align-items: center; + gap: 18px; +} + +.salary-profile-list-tools { + flex: 1 1 520px; + min-width: min(620px, 100%); +} + +.salary-profile-toolbar { + display: grid; + grid-template-columns: minmax(240px, 1fr) auto auto; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + +.salary-profile-list-tools input { + min-height: 36px; + width: 100%; + border: 1px solid var(--line); + border-radius: var(--radius-md); + background: var(--control-bg); + color: var(--text); + outline: 0; + padding: 0 12px; + transition: + border-color 0.18s ease, + box-shadow 0.18s ease; +} + +.salary-profile-list-tools input:focus { + border-color: var(--blue); + box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.12); +} + +.salary-profile-table { + min-width: 1120px; +} + +.salary-profile-table th:nth-child(1), +.salary-profile-table th:nth-child(2) { + width: 16%; +} + +.salary-profile-table th:nth-child(3), +.salary-profile-table th:nth-child(7), +.salary-profile-table th:nth-child(8) { + width: 9%; +} + +.salary-profile-table .link-button { + white-space: nowrap; +} + +.salary-profile-pay-cell { + display: grid; + gap: 4px; +} + +.salary-profile-pay-cell strong { + color: var(--primary); +} + +.salary-profile-pay-cell span { + margin: 0; +} + +.salary-profile-rate-stack { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.salary-profile-rate-stack span { + display: inline-flex; + min-height: 24px; + align-items: center; + border-radius: var(--radius-sm); + background: var(--green-soft); + color: var(--green); + padding: 0 8px; + font-size: 12px; + font-weight: 800; +} + +.salary-profile-edit-modal { + align-items: start; + padding: 24px; + padding-top: 28px; +} + +.salary-profile-edit-card { + width: min(980px, 100%); + max-height: min(780px, calc(100vh - 48px)); + overflow: auto; +} + +.salary-profile-modal-form { + display: grid; + gap: 12px; + border-top: 1px solid var(--line-soft); + padding: 16px; +} + +.salary-profile-modal-section { + display: grid; + gap: 12px; + border: var(--card-inner-border); + border-radius: var(--radius-md); + background: var(--card-inner-bg); + box-shadow: var(--card-inner-shadow); + padding: 14px; +} + +.salary-profile-modal-section-muted { + background: var(--card); +} + +.salary-profile-modal-section-title { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + border-bottom: 1px solid var(--line-soft); + padding-bottom: 10px; +} + +.salary-profile-modal-section-title strong { + color: var(--primary); + font-size: 15px; +} + +.salary-profile-modal-section-title span { + color: var(--muted); + font-size: 12px; + font-weight: 700; +} + +.salary-profile-modal-fields { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.salary-profile-modal-section .field { + gap: 6px; +} + +.salary-profile-modal-section .field span { + font-size: 12px; +} + +.salary-profile-modal-section .field input, +.salary-profile-modal-section .field select, +.salary-profile-modal-section .field textarea { + min-height: 38px; +} + +.salary-profile-modal-section .field textarea { + min-height: 74px; +} + +.salary-profile-modal-message { + justify-self: center; +} + +.salary-profile-modal-actions { + justify-content: flex-end; + border-top: 0; + padding: 2px 0 0; +} + +.report-page { + gap: 12px; +} + +.report-ledger-workspace { + min-height: 460px; +} + +.report-ledger-toolbar { + display: grid; + grid-template-columns: minmax(280px, 0.72fr) minmax(420px, 1.28fr); + align-items: stretch; + gap: 10px; + border-bottom: 1px solid var(--line-soft); + padding: 10px var(--panel-padding) 8px; +} + +.report-ledger-title { + display: flex; + min-width: 0; + align-items: center; + gap: 12px; +} + +.report-ledger-title h2 { + margin: 2px 0 4px; + color: var(--primary); + font-size: 18px; + line-height: 1.25; +} + +.report-ledger-eyebrow { + color: var(--muted); + font-size: 12px; + font-weight: 800; +} + +.report-ledger-totals { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.report-total-item { + display: grid; + min-width: 0; + min-height: 52px; + align-content: center; + gap: 3px; + border-radius: var(--radius-md); + padding: 7px 10px; +} + +.report-total-item.is-primary { + border-color: rgba(var(--accent-rgb), 0.24); + background: var(--blue-soft); +} + +.report-total-item span, +.report-attendance-card span { + color: var(--muted); + font-size: 12px; + font-weight: 800; +} + +.report-total-item strong { + overflow: hidden; + color: var(--primary); + font-size: 17px; + line-height: 1.18; + text-overflow: ellipsis; + white-space: nowrap; +} + +.report-total-item small { + overflow: hidden; + color: var(--muted); + font-size: 11px; + font-weight: 700; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.report-inline-metrics { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + border-bottom: 1px solid var(--line-soft); + background: var(--surface); + padding: 7px var(--panel-padding); +} + +.report-inline-metrics span { + display: inline-flex; + min-height: 28px; + min-width: 0; + align-items: center; + gap: 6px; + border-radius: var(--radius-md); + color: var(--muted); + padding: 4px 8px; + font-size: 12px; + font-weight: 800; + line-height: 1.2; +} + +.report-inline-metrics svg { + flex: 0 0 auto; + color: var(--blue); +} + +.report-inline-metrics strong { + color: var(--primary); + font-size: 13px; + line-height: 1.2; + white-space: nowrap; +} + +.report-table-heading { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 12px; + padding: 9px var(--panel-padding) 6px; +} + +.report-table-heading h3 { + margin: 0; + color: var(--primary); + font-size: 15px; + line-height: 1.3; +} + +.report-table-heading p { + margin: 3px 0 0; + color: var(--muted); + font-size: 12px; + line-height: 1.35; +} + +.report-detail-grid { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.65fr); + gap: var(--content-gap); + align-items: start; +} + +.report-attendance-list { + display: grid; + gap: 10px; + padding: var(--panel-padding); +} + +.report-attendance-card { + display: flex; + min-width: 0; + min-height: 54px; + align-items: center; + justify-content: space-between; + gap: 12px; + border-radius: var(--radius-md); + padding: 12px; +} + +.report-attendance-card strong { + color: var(--primary); + font-size: 20px; + line-height: 1.1; +} + +.report-record-panel { + min-height: 460px; +} + +.report-record-table { + min-width: 1180px; +} + +.report-net-cell { + color: var(--green); } .organization-summary-strip { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; - border: 1px solid var(--line); - border-radius: var(--radius-lg); - background: var(--card); - box-shadow: 0 8px 20px rgba(var(--primary-rgb), 0.04); padding: 10px; } @@ -1889,8 +2375,10 @@ a { align-items: center; gap: 10px; min-height: 50px; + border: var(--card-inner-border); border-radius: var(--radius-md); - background: var(--surface); + background: var(--card-inner-bg); + box-shadow: var(--card-inner-shadow); padding: 10px 12px; } @@ -2078,9 +2566,10 @@ a { .organization-node-summary div { min-width: 0; - border: 1px solid var(--line-soft); + border: var(--card-inner-border); border-radius: var(--radius-md); - background: var(--card); + background: var(--card-inner-bg); + box-shadow: var(--card-inner-shadow); padding: 10px 12px; } @@ -2115,15 +2604,16 @@ a { } .workflow-steps span { - border: 1px solid var(--line); + border: var(--card-inner-border); border-radius: var(--radius-md); - background: var(--surface); - padding: 9px 10px; + background: var(--card-inner-bg); + box-shadow: var(--card-inner-shadow); + padding: 8px 10px; text-align: center; } .user-form { - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(4, minmax(0, 1fr)); } .span-row { @@ -2132,13 +2622,13 @@ a { .field { display: grid; - gap: 8px; + gap: 6px; } .field span, .checkbox-card span { color: var(--muted); - font-size: 13px; + font-size: 12px; font-weight: 800; } @@ -2161,13 +2651,13 @@ a { .field input, .field select, .field textarea { - padding: 0 13px; + padding: 0 12px; } .field textarea { - min-height: 112px; + min-height: 96px; resize: vertical; - padding: 12px 13px; + padding: 10px 12px; } .field input:focus, @@ -2191,20 +2681,21 @@ a { border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--control-bg); - padding: 0 14px; + padding: 0 12px; } .form-actions { border-top: 1px solid var(--line-soft); - padding: 18px 24px 24px; + background: var(--surface); + padding: 12px var(--panel-padding) var(--panel-padding); } .search-form { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; - gap: 16px; - padding: 24px; + gap: 12px; + padding: var(--panel-padding); } .table-filter-bar { @@ -2213,7 +2704,8 @@ a { align-items: end; gap: 12px; border-bottom: 1px solid var(--line-soft); - padding: 14px var(--panel-padding); + background: var(--surface); + padding: 10px var(--panel-padding); } .table-filter-bar .field { @@ -2222,16 +2714,17 @@ a { .table-filter-bar .field input, .table-filter-bar .field select { - min-height: 40px; + min-height: 36px; } .table-pagination { display: flex; align-items: center; justify-content: space-between; - gap: 16px; + gap: 12px; border-top: 1px solid var(--line-soft); - padding: 18px 24px 24px; + background: var(--surface); + padding: 12px var(--panel-padding) var(--panel-padding); } .table-pagination p { @@ -2241,7 +2734,9 @@ a { } .table-pagination .form-actions { + justify-content: flex-end; border-top: 0; + background: transparent; padding: 0; } @@ -2256,18 +2751,18 @@ a { } .compact-select { - min-height: 38px; + min-height: 36px; padding: 0 12px; } .table-input { - min-height: 36px; + min-height: 34px; width: min(180px, 100%); padding: 0 10px; } .table-textarea { - min-height: 54px; + min-height: 48px; width: min(320px, 100%); resize: vertical; padding: 8px 10px; @@ -2288,11 +2783,15 @@ a { .data-table td { border-bottom: 1px solid var(--line-soft); padding: var(--table-cell-y) var(--table-cell-x); + line-height: 1.35; text-align: left; vertical-align: middle; } .data-table th { + position: sticky; + top: 0; + z-index: 1; background: var(--table-header-bg); color: var(--muted); font-size: 12px; @@ -2303,6 +2802,10 @@ a { color: var(--text); } +.data-table tbody tr { + transition: background var(--motion-duration-fast) ease; +} + .data-table tr:hover td { background: var(--table-row-hover-bg); } @@ -2332,6 +2835,10 @@ a { color: var(--muted); } +.link-button.danger { + color: var(--red); +} + .operation-log-table { min-width: 1080px; } @@ -2353,29 +2860,30 @@ a { .empty-state { display: grid; - min-height: 150px; + min-height: 120px; place-items: center; color: var(--muted); - padding: 24px; + padding: var(--panel-padding); } .recent-list { display: grid; gap: 10px; - padding: 18px 24px 8px; + padding: 14px var(--panel-padding) 8px; } .recent-item { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; - gap: 14px; + gap: 12px; width: 100%; - border: 1px solid var(--line-soft); + border: var(--card-inner-border); border-radius: var(--radius-md); - background: var(--control-bg); + background: var(--card-inner-bg); + box-shadow: var(--card-inner-shadow); color: var(--text); - padding: 14px; + padding: 12px; text-align: left; } @@ -2400,10 +2908,19 @@ a { .form-error, .form-success { + position: fixed; + top: calc(var(--topbar-height) + 18px); + right: auto; + left: 50%; + z-index: 120; + width: min(380px, calc(100vw - 32px)); margin: 0; border-radius: var(--radius-md); padding: 12px 14px; + box-shadow: 0 18px 42px rgba(var(--primary-rgb), 0.16); font-weight: 700; + pointer-events: none; + animation: toast-autohide 4.2s ease forwards; } .form-error { @@ -2420,6 +2937,27 @@ a { animation: spin 0.9s linear infinite; } +@keyframes toast-autohide { + 0% { + opacity: 0; + transform: translate(-50%, -8px); + visibility: visible; + } + + 10%, + 78% { + opacity: 1; + transform: translate(-50%, 0); + visibility: visible; + } + + 100% { + opacity: 0; + transform: translate(-50%, -8px); + visibility: hidden; + } +} + .login-screen { display: grid; min-height: 100vh; @@ -2599,20 +3137,29 @@ a { grid-template-columns: repeat(3, minmax(0, 1fr)); } - .stats-grid, + .salary-profile-summary-strip, .dashboard-grid, .action-grid, .compact-stats-grid, .form-grid, + .salary-profile-modal-fields, .workflow-steps, .password-form { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .report-ledger-toolbar { + grid-template-columns: 1fr; + } + .organization-tree-layout { grid-template-columns: 1fr; } + .report-detail-grid { + grid-template-columns: 1fr; + } + .login-screen { grid-template-columns: 1fr; } @@ -2723,12 +3270,15 @@ a { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .stats-grid, + .salary-profile-summary-strip, .organization-summary-strip, + .report-ledger-toolbar, + .report-ledger-totals, .dashboard-grid, .action-grid, .compact-stats-grid, .form-grid, + .salary-profile-modal-fields, .user-form, .password-form, .search-form, @@ -2763,6 +3313,14 @@ a { font-size: 27px; } + .form-error, + .form-success { + top: 18px; + right: auto; + left: 50%; + width: min(360px, calc(100vw - 32px)); + } + .organization-tree { max-height: none; min-height: 360px; @@ -2777,6 +3335,70 @@ a { justify-content: flex-start; } + .summary-strip { + padding: 6px; + } + + .summary-metric { + min-width: 0; + flex-basis: 100%; + } + + .report-ledger-toolbar { + padding: 12px; + } + + .report-ledger-title { + align-items: flex-start; + flex-direction: column; + gap: 8px; + } + + .report-total-item strong { + font-size: 16px; + } + + .report-inline-metrics { + align-items: stretch; + flex-direction: column; + } + + .report-inline-metrics span { + justify-content: space-between; + width: 100%; + } + + .salary-profile-list-tools { + width: 100%; + justify-content: flex-start; + } + + .salary-profile-toolbar { + grid-template-columns: 1fr; + } + + .salary-profile-toolbar .compact-button { + width: 100%; + } + + .salary-profile-list-tools input { + width: 100%; + } + + .salary-profile-edit-modal { + padding: 14px; + } + + .salary-profile-modal-form { + padding: 12px; + } + + .salary-profile-modal-section-title { + align-items: flex-start; + flex-direction: column; + gap: 4px; + } + .organization-node-summary { grid-template-columns: 1fr; } diff --git a/frontend/src/components/StatCard.vue b/frontend/src/components/StatCard.vue index 94737a4..0236964 100644 --- a/frontend/src/components/StatCard.vue +++ b/frontend/src/components/StatCard.vue @@ -11,14 +11,14 @@ defineProps<{ diff --git a/frontend/src/stores/theme.ts b/frontend/src/stores/theme.ts index 7d07723..3df2727 100644 --- a/frontend/src/stores/theme.ts +++ b/frontend/src/stores/theme.ts @@ -744,48 +744,48 @@ function radiusVariables(radius: RadiusChoice): Record { function densityVariables(density: DensityChoice): Record { const densityMap: Record> = { compact: { - "--topbar-height": "64px", - "--nav-height": "42px", - "--control-height": "40px", - "--base-font-size": "15px", - "--page-padding": "24px", - "--content-gap": "18px", - "--panel-padding": "20px", - "--table-cell-y": "12px", - "--table-cell-x": "18px", + "--topbar-height": "60px", + "--nav-height": "38px", + "--control-height": "38px", + "--base-font-size": "14px", + "--page-padding": "20px", + "--content-gap": "14px", + "--panel-padding": "16px", + "--table-cell-y": "10px", + "--table-cell-x": "14px", }, default: { + "--topbar-height": "66px", + "--nav-height": "44px", + "--control-height": "42px", + "--base-font-size": "15px", + "--page-padding": "26px", + "--content-gap": "18px", + "--panel-padding": "20px", + "--table-cell-y": "14px", + "--table-cell-x": "18px", + }, + comfortable: { "--topbar-height": "72px", - "--nav-height": "48px", - "--control-height": "44px", + "--nav-height": "50px", + "--control-height": "46px", "--base-font-size": "16px", "--page-padding": "32px", "--content-gap": "24px", "--panel-padding": "24px", "--table-cell-y": "18px", - "--table-cell-x": "24px", - }, - comfortable: { - "--topbar-height": "78px", - "--nav-height": "54px", - "--control-height": "48px", - "--base-font-size": "16px", - "--page-padding": "36px", - "--content-gap": "28px", - "--panel-padding": "28px", - "--table-cell-y": "22px", - "--table-cell-x": "28px", + "--table-cell-x": "22px", }, large: { - "--topbar-height": "88px", - "--nav-height": "62px", - "--control-height": "56px", - "--base-font-size": "18px", - "--page-padding": "44px", - "--content-gap": "34px", - "--panel-padding": "34px", - "--table-cell-y": "28px", - "--table-cell-x": "32px", + "--topbar-height": "82px", + "--nav-height": "58px", + "--control-height": "52px", + "--base-font-size": "17px", + "--page-padding": "38px", + "--content-gap": "30px", + "--panel-padding": "30px", + "--table-cell-y": "24px", + "--table-cell-x": "28px", }, }; return densityMap[density]; diff --git a/frontend/src/views/CommissionsView.vue b/frontend/src/views/CommissionsView.vue index 165c0d6..621646c 100644 --- a/frontend/src/views/CommissionsView.vue +++ b/frontend/src/views/CommissionsView.vue @@ -229,7 +229,7 @@ function money(value: number): string { -
+
diff --git a/frontend/src/views/ConfigCenterView.vue b/frontend/src/views/ConfigCenterView.vue index ba492d3..664f5e2 100644 --- a/frontend/src/views/ConfigCenterView.vue +++ b/frontend/src/views/ConfigCenterView.vue @@ -95,7 +95,7 @@ async function saveConfig(config: SalaryConfigRecord): Promise { -
+
diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index 1166741..faaa0c1 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -44,7 +44,7 @@ async function download(job: RecentPayrollJob): Promise { -
+
-
+
diff --git a/frontend/src/views/PayrollView.vue b/frontend/src/views/PayrollView.vue index d23993e..c6f2717 100644 --- a/frontend/src/views/PayrollView.vue +++ b/frontend/src/views/PayrollView.vue @@ -226,7 +226,7 @@ function money(value: number): string {

{{ errorMessage }}

{{ successMessage }}

-
+
diff --git a/frontend/src/views/ReportsView.vue b/frontend/src/views/ReportsView.vue index ddb4283..15ae43e 100644 --- a/frontend/src/views/ReportsView.vue +++ b/frontend/src/views/ReportsView.vue @@ -5,7 +5,6 @@ import { computed, onMounted, ref } from "vue"; import { ApiError } from "../api/http"; import { getPayrollReport } from "../api/reports"; import type { PayrollReportResponse } from "../api/types"; -import StatCard from "../components/StatCard.vue"; const salaryMonth = ref(new Date().toISOString().slice(0, 7)); const report = ref(null); @@ -49,7 +48,7 @@ function modeLabel(value: string): string { diff --git a/frontend/src/views/SalaryProfilesView.vue b/frontend/src/views/SalaryProfilesView.vue index 772ec49..5e44ee0 100644 --- a/frontend/src/views/SalaryProfilesView.vue +++ b/frontend/src/views/SalaryProfilesView.vue @@ -4,11 +4,13 @@ import { HandCoins, Loader2, Pencil, + Plus, RefreshCw, Save, Search, TimerReset, WalletCards, + X, } from "@lucide/vue"; import { computed, onMounted, reactive, ref } from "vue"; @@ -16,12 +18,12 @@ import { listEmployees } from "../api/employees"; import { ApiError } from "../api/http"; import { listSalaryProfiles, saveSalaryProfile } from "../api/salaryProfiles"; import type { EmployeeRecord, SalaryProfileRecord, SalaryProfileRequest } from "../api/types"; -import StatCard from "../components/StatCard.vue"; const employees = ref([]); const profiles = ref([]); const loading = ref(false); const saving = ref(false); +const showEditor = ref(false); const errorMessage = ref(""); const successMessage = ref(""); const keyword = ref(""); @@ -59,6 +61,9 @@ const avgOvertimeRate = computed(() => { } return profiles.value.reduce((total, item) => total + Number(item.overtime_rate || 0), 0) / profiles.value.length; }); +const editorTitle = computed(() => { + return profiles.value.some((item) => item.employee_id === form.employee_id) ? "编辑薪资" : "新增薪资"; +}); onMounted(loadData); @@ -108,6 +113,7 @@ async function submit(): Promise { }); profiles.value = [profile, ...profiles.value.filter((item) => item.employee_id !== profile.employee_id)]; successMessage.value = "薪资档案已保存"; + showEditor.value = false; } catch (error) { errorMessage.value = error instanceof ApiError ? error.message : "薪资档案保存失败"; } finally { @@ -135,6 +141,18 @@ function editProfile(profile: SalaryProfileRecord): void { }); successMessage.value = ""; errorMessage.value = ""; + showEditor.value = true; +} + +function openCreateProfile(): void { + resetForm(); + successMessage.value = ""; + errorMessage.value = ""; + showEditor.value = true; +} + +function closeEditor(): void { + showEditor.value = false; } function resetForm(): void { @@ -186,7 +204,7 @@ function money(value: number): string {