├── .github
└── workflows
│ └── docker-build.yml
├── Dockerfile
├── README.md
├── docker-compose.yml
├── index.html
├── nginx.conf
├── script.js
├── styles.css
└── version.js
/.github/workflows/docker-build.yml:
--------------------------------------------------------------------------------
1 | name: Build and Push Docker Image
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - main
8 | tags:
9 | - 'v*'
10 |
11 | env:
12 | IMAGE_NAME: bobby567/calculator
13 |
14 | jobs:
15 | build-and-push:
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - name: Checkout code
20 | uses: actions/checkout@v4
21 |
22 | - name: Login to Docker Hub
23 | uses: docker/login-action@v3
24 | with:
25 | username: ${{ secrets.DOCKER_USERNAME }}
26 | password: ${{ secrets.DOCKER_TOKEN }}
27 |
28 | - name: Set up QEMU
29 | uses: docker/setup-qemu-action@v3
30 |
31 | - name: Set up Docker Buildx
32 | uses: docker/setup-buildx-action@v3
33 |
34 | - name: Build and push Docker image
35 | uses: docker/build-push-action@v5
36 | with:
37 | context: .
38 | platforms: linux/amd64,linux/arm64,linux/arm/v7
39 | push: true
40 | tags: ${{ env.IMAGE_NAME }}:latest
41 | cache-from: type=gha
42 | cache-to: type=gha,mode=max
43 |
44 | - name: Build success
45 | run: |
46 | echo "Docker image ${{ env.IMAGE_NAME }}:latest has been successfully built and pushed"
47 | echo "Supported architectures: linux/amd64, linux/arm64, linux/arm/v7"
48 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:alpine
2 |
3 | # Install necessary packages
4 | RUN apk add --no-cache curl
5 |
6 | # Set the working directory
7 | WORKDIR /usr/share/nginx/html
8 |
9 | # Copy the application files
10 | COPY index.html .
11 | COPY script.js .
12 | COPY styles.css .
13 | COPY version.js .
14 |
15 | # Copy custom nginx configuration
16 | COPY nginx.conf /etc/nginx/nginx.conf
17 |
18 | # Expose port 80
19 | EXPOSE 80
20 |
21 | # Set healthcheck
22 | HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/ || exit 1
23 |
24 | # Command to run the application
25 | CMD ["nginx", "-g", "daemon off;"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 剩余价值计算器 V4
2 |
3 | > 一个基于 Material Design 3 的现代化剩余价值计算器,帮助用户精确计算主机、域名等周期性服务的剩余价值。
4 |
5 | ## 🚀 在线体验
6 |
7 | | 演示地址 | 状态 |
8 | |---------|------|
9 | | [主站点](https://) | 暂时关闭 |
10 | | [Cloudflare Pages](https://vps-calculator-docker.pages.dev/) | ✅ 可用 |
11 |
12 | ## 📱 界面预览
13 |
14 |
15 | 点击查看界面截图
16 |
17 | ### 亮色模式
18 |
19 |
20 | ### 暗色模式
21 |
22 |
23 |
24 |
25 |
26 | ## ✨ 核心功能
27 |
28 | | 功能 | 描述 |
29 | |------|------|
30 | | 💱 **多币种支持** | 支持 USD、EUR、CNY 等 11 种主流货币 |
31 | | 🔄 **实时汇率** | 自动获取最新汇率数据 |
32 | | 📅 **灵活周期** | 月付到五年付 |
33 | | 🎯 **精准计算** | 自动计算剩余天数和价值 |
34 | | 🎨 **全新界面** | 采用 Material Design 3 设计语言
35 | | 🐳 **容器化部署** | 支持 Docker 一键部署
36 | | 📷 **截图功能** | 一键截图并上传到图床
37 | | 🌓 **主题模式** | 支持亮色/暗色模式切换
38 |
39 |
40 |
41 | ## 🚀 快速部署
42 |
43 | ### 🐳 Docker 部署
44 |
45 | ```bash
46 | docker run -d --name=jsq -p 8089:80 bobby567/calculator:latest
47 | ```
48 | 如果出现异常,请先尝试执行 ``docker pull bobby567/calculator`` 或更换端口
49 |
50 | ### ☁️ 其他方式部署
51 |
52 | | 平台 | 部署方式 |
53 | |------|----------|
54 | | **Cloudflare Pages** | Fork 本仓库,连接到 Cloudflare Pages |
55 | | **GitHub Pages** | 启用 GitHub Pages,选择 main 分支 |
56 | | **Vercel** | 导入项目,自动部署 |
57 | | **Netlify** | 拖拽文件夹或连接 Git 仓库 |
58 |
59 |
60 | ## 🔗 相关链接
61 |
62 | | 项目 | 链接 |
63 | |------|------|
64 | | 🏠 **本项目** | [GitHub](https://github.com/realnovicedev/vps_calculator_docker) |
65 | | 🌱 **原项目** | [vps_surplus_value](https://github.com/Tomzhao1016/vps_surplus_value) |
66 | | 📖 **Material Web** | [官方文档](https://material-web.dev/) |
67 |
68 | ## 🙏 致谢
69 |
70 | 感谢以下项目和个人的贡献:
71 |
72 | - [pengzhile](https://linux.do/t/topic/227730/26) - 实时汇率 API 代码
73 | - [Dooongの公益图床](https://www.nodeseek.com/post-43196-1) - 提供图床服务
74 | - [NodeSeek 编辑器增强脚本](https://www.nodeseek.com/post-74493-1) - 图床上传代码参考
75 | - mjj - 大鸡腿
76 |
77 | ## 📄 许可协议
78 |
79 | 本项目遵循原项目的开源许可协议。
80 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | vps-calculator:
5 | build: .
6 | ports:
7 | - "8089:80"
8 | restart: unless-stopped
9 | volumes:
10 | - ./:/usr/share/nginx/html
11 | environment:
12 | - TZ=Asia/Shanghai
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 剩余价值计算器
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
98 |
99 |
112 |
113 |
120 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | 剩余价值计算器
152 |
153 |
154 |
155 |
156 |
157 |
216 |
217 |
269 |
270 |
288 |
289 |
290 |
291 |
292 | 计算剩余价值
293 |
294 |
295 |
296 |
297 |
298 |
312 |
313 |
314 |
315 |
316 |
317 | 交易日期
318 |
319 |
0000-00-00
320 |
321 |
322 |
323 |
324 |
325 |
326 | 外币汇率
327 |
328 |
0.00
329 |
330 |
331 |
332 |
333 |
334 |
335 | 续费价格
336 |
337 |
0.00 人民币/年
338 |
339 |
340 |
341 |
342 |
343 |
344 | 剩余天数
345 |
346 |
0
347 |
348 |
349 |
350 |
351 |
352 |
353 | 到期日期
354 |
355 |
0000-00-00
356 |
357 |
358 |
359 |
360 |
361 |
362 | 剩余价值
363 |
364 |
0.000 元
365 |
366 |
367 |
368 |
369 |
370 |
371 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
466 |
467 |
468 |
469 |
470 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 | user nginx;
2 | worker_processes auto;
3 |
4 | error_log /var/log/nginx/error.log warn;
5 | pid /var/run/nginx.pid;
6 |
7 | events {
8 | worker_connections 1024;
9 | }
10 |
11 | http {
12 | include /etc/nginx/mime.types;
13 | default_type application/octet-stream;
14 |
15 | log_format main '$remote_addr - $remote_user [$time_local] "$request" '
16 | '$status $body_bytes_sent "$http_referer" '
17 | '"$http_user_agent" "$http_x_forwarded_for"';
18 |
19 | access_log /var/log/nginx/access.log main;
20 |
21 | sendfile on;
22 | tcp_nopush on;
23 | tcp_nodelay on;
24 | keepalive_timeout 65;
25 | types_hash_max_size 2048;
26 |
27 | # Gzip compression
28 | gzip on;
29 | gzip_comp_level 5;
30 | gzip_min_length 256;
31 | gzip_proxied any;
32 | gzip_vary on;
33 | gzip_types
34 | application/javascript
35 | application/json
36 | application/xml
37 | application/xml+rss
38 | text/css
39 | text/javascript
40 | text/plain
41 | text/xml;
42 |
43 | server {
44 | listen 80;
45 | server_name localhost;
46 |
47 | # Security headers
48 | add_header X-Content-Type-Options "nosniff";
49 | add_header X-XSS-Protection "1; mode=block";
50 | add_header X-Frame-Options "SAMEORIGIN";
51 | add_header Referrer-Policy "no-referrer-when-downgrade";
52 |
53 | location / {
54 | root /usr/share/nginx/html;
55 | index index.html index.htm;
56 | try_files $uri $uri/ /index.html;
57 | }
58 |
59 | # Media files caching
60 | location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
61 | root /usr/share/nginx/html;
62 | expires 1d;
63 | add_header Cache-Control "public, max-age=86400";
64 | }
65 |
66 | # Error handling
67 | error_page 500 502 503 504 /50x.html;
68 | location = /50x.html {
69 | root /usr/share/nginx/html;
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 |
2 | (function() {
3 | const storedVersion = localStorage.getItem('app_version');
4 | if (storedVersion !== APP_VERSION) {
5 | console.log('检测到新版本,清除缓存...');
6 | const theme = localStorage.getItem('theme');
7 | const imgHostSettings = localStorage.getItem('imgHostSettings');
8 | localStorage.clear();
9 | if (theme) localStorage.setItem('theme', theme);
10 | if (imgHostSettings) localStorage.setItem('imgHostSettings', imgHostSettings);
11 | localStorage.setItem('app_version', APP_VERSION);
12 | }
13 | })();
14 |
15 | const imgHost = {
16 | type: "LskyPro", // 图床类型, 仅支持 LskyPro / EasyImages
17 | url: "https://image.dooo.ng", // 图床地址, 带上协议头
18 | token: "", // LskyPro 可为空则使用游客上传, 在 /user/tokens 生成
19 | copyFormat: "markdown" // 默认为URL格式
20 | };
21 |
22 | document.addEventListener('DOMContentLoaded', function() {
23 |
24 | function showPageAndInitialize() {
25 | if (document.body.classList.contains('is-loading')) {
26 | document.body.style.visibility = 'visible';
27 | document.body.classList.remove('is-loading');
28 | runInitializations();
29 | }
30 | }
31 |
32 | const keyComponents = [
33 | 'md-outlined-text-field',
34 | 'md-outlined-select',
35 | 'md-filled-button'
36 | ];
37 | const componentPromises = keyComponents.map(tag => customElements.whenDefined(tag));
38 | Promise.race(componentPromises).then(() => {
39 | clearTimeout(safetyTimeout);
40 | showPageAndInitialize();
41 | }).catch(error => {
42 | clearTimeout(safetyTimeout);
43 | showPageAndInitialize();
44 | });
45 |
46 | const safetyTimeout = setTimeout(() => {
47 | showPageAndInitialize();
48 | }, 3000); // 3秒超时
49 |
50 | function runInitializations() {
51 | // 初始化主题
52 | initTheme();
53 |
54 | // 初始化日期选择器
55 | flatpickr.localize(flatpickr.l10ns.zh);
56 | initializeDatePickers();
57 |
58 | // 初始化其他功能
59 | fetchExchangeRate();
60 | setDefaultTransactionDate();
61 |
62 | // 初始化图床设置
63 | initSettings();
64 |
65 | // 统一添加所有事件监听器
66 | document.getElementById('currency').addEventListener('change', fetchExchangeRate);
67 | document.getElementById('calculateBtn').addEventListener('click', calculateAndSend);
68 | document.getElementById('copyLinkBtn').addEventListener('click', copyLink);
69 | document.getElementById('screenshotBtn').addEventListener('click', captureAndUpload);
70 |
71 | // 等待Material Web组件加载完成后添加事件监听器
72 | setTimeout(() => {
73 | const currencySelect = document.getElementById('currency');
74 | if (currencySelect && currencySelect.addEventListener) {
75 | currencySelect.addEventListener('change', fetchExchangeRate);
76 | }
77 | }, 100);
78 |
79 | initSettings();
80 |
81 | // 添加设置按钮事件监听 - 适配侧边栏
82 | document.getElementById('settingsToggle').addEventListener('click', openSettingsSidebar);
83 | document.getElementById('closeSidebar').addEventListener('click', closeSettingsSidebar);
84 | document.getElementById('sidebarOverlay').addEventListener('click', closeSettingsSidebar);
85 | document.getElementById('saveSettings').addEventListener('click', saveSettings);
86 | document.getElementById('resetSettings').addEventListener('click', resetSettings);
87 | document.querySelector('.toggle-password').addEventListener('click', togglePasswordVisibility);
88 |
89 | // ESC键关闭侧边栏
90 | document.addEventListener('keydown', function(e) {
91 | if (e.key === 'Escape') {
92 | closeSettingsSidebar();
93 | }
94 | });
95 | }
96 | populateFormFromUrlAndCalc();
97 | });
98 |
99 | function populateFormFromUrlAndCalc() {
100 | const urlParams = new URLSearchParams(window.location.search);
101 | if (urlParams.toString() === '') {
102 | return; // No params, use default behavior
103 | }
104 |
105 | if (urlParams.has('currency')) {
106 | document.getElementById('currency').value = urlParams.get('currency');
107 | }
108 | if (urlParams.has('price')) {
109 | document.getElementById('amount').value = urlParams.get('price');
110 | }
111 | if (urlParams.has('cycle')) {
112 | document.getElementById('cycle').value = urlParams.get('cycle');
113 | }
114 | if (urlParams.has('due')) {
115 | const expiryDate = urlParams.get('due');
116 | if (expiryDate.match(/^\d{8}$/)) {
117 | const formattedDate = `${expiryDate.substring(0, 4)}-${expiryDate.substring(4, 6)}-${expiryDate.substring(6, 8)}`;
118 | document.getElementById('expiryDate').value = formattedDate;
119 | }
120 | }
121 |
122 | const fetchPromise = fetchExchangeRate(true);
123 |
124 | fetchPromise.then(() => {
125 | if (urlParams.has('rate')) {
126 | document.getElementById('customRate').value = urlParams.get('rate');
127 | }
128 | setTimeout(() => {
129 | calculateAndSend();
130 | }, 100);
131 | });
132 | }
133 |
134 | // 主题切换功能
135 | function initTheme() {
136 | const themeToggle = document.getElementById('themeToggle');
137 | const themeIcon = themeToggle.querySelector('i');
138 | const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
139 |
140 | // 检查本地存储中的主题设置
141 | const currentTheme = localStorage.getItem('theme');
142 |
143 | // 应用保存的主题或系统主题
144 | if (currentTheme === 'dark') {
145 | document.documentElement.setAttribute('data-theme', 'dark');
146 | themeIcon.className = 'fas fa-sun';
147 | } else if (currentTheme === 'light') {
148 | document.documentElement.setAttribute('data-theme', 'light');
149 | themeIcon.className = 'fas fa-moon';
150 | } else if (prefersDarkScheme.matches) {
151 | // 如果没有保存的主题但系统偏好暗色模式
152 | document.documentElement.setAttribute('data-theme', 'dark');
153 | themeIcon.className = 'fas fa-sun';
154 | localStorage.setItem('theme', 'dark');
155 | } else {
156 | // 默认使用亮色主题
157 | document.documentElement.setAttribute('data-theme', 'light');
158 | themeIcon.className = 'fas fa-moon';
159 | localStorage.setItem('theme', 'light');
160 | }
161 |
162 | // 切换主题
163 | themeToggle.addEventListener('click', function() {
164 | let theme;
165 | if (document.documentElement.getAttribute('data-theme') === 'dark') {
166 | document.documentElement.setAttribute('data-theme', 'light');
167 | theme = 'light';
168 | themeIcon.className = 'fas fa-moon';
169 | } else {
170 | document.documentElement.setAttribute('data-theme', 'dark');
171 | theme = 'dark';
172 | themeIcon.className = 'fas fa-sun';
173 | }
174 |
175 | // 保存主题设置到本地存储
176 | localStorage.setItem('theme', theme);
177 | });
178 | }
179 |
180 | function initializeDatePickers() {
181 | flatpickr("#expiryDate", {
182 | dateFormat: "Y-m-d",
183 | locale: "zh",
184 | placeholder: "选择到期日期",
185 | minDate: "today",
186 | onChange: function(_selectedDates, dateStr) {
187 | const transactionPicker = document.getElementById('transactionDate')._flatpickr;
188 | transactionPicker.set('maxDate', dateStr);
189 | validateDates();
190 | }
191 | });
192 |
193 | flatpickr("#transactionDate", {
194 | dateFormat: "Y-m-d",
195 | locale: "zh",
196 | placeholder: "选择交易日期",
197 | onChange: validateDates
198 | });
199 | }
200 |
201 | function validateDates() {
202 | const expiryDateInput = document.getElementById('expiryDate').value;
203 | const transactionDateInput = document.getElementById('transactionDate').value;
204 |
205 | if (!expiryDateInput || !transactionDateInput) return;
206 |
207 | const expiryDate = new Date(expiryDateInput);
208 | const transactionDate = new Date(transactionDateInput);
209 | const today = new Date();
210 |
211 | // 设置所有时间为当天的开始(00:00:00)
212 | expiryDate.setHours(0, 0, 0, 0);
213 | transactionDate.setHours(0, 0, 0, 0);
214 | today.setHours(0, 0, 0, 0);
215 |
216 | if (expiryDate <= today) {
217 | showNotification('到期日期必须晚于今天', 'error');
218 | document.getElementById('expiryDate').value = '';
219 | return;
220 | }
221 |
222 | if (transactionDate > expiryDate) {
223 | showNotification('交易日期不能晚于到期日期', 'error');
224 | setDefaultTransactionDate();
225 | return;
226 | }
227 |
228 | if (expiryDate.getTime() === transactionDate.getTime()) {
229 | showNotification('交易日期不能等于到期日期', 'error');
230 | setDefaultTransactionDate();
231 | return;
232 | }
233 |
234 | updateRemainingDays();
235 | }
236 |
237 | function updateRemainingDays() {
238 | const expiryDate = document.getElementById('expiryDate').value;
239 | const transactionDate = document.getElementById('transactionDate').value;
240 |
241 | if (expiryDate && transactionDate) {
242 | const remainingDays = calculateRemainingDays(expiryDate, transactionDate);
243 |
244 | // 检查是否存在remainingDays元素
245 | const remainingDaysElement = document.getElementById('remainingDays');
246 | if (remainingDaysElement) {
247 | remainingDaysElement.textContent = remainingDays;
248 |
249 | if (remainingDays === 0) {
250 | showNotification('剩余天数为0,请检查日期设置', 'warning');
251 | }
252 | }
253 | }
254 | }
255 |
256 | /**
257 | * 实时汇率获取 @pengzhile
258 | * 代码来源: https://linux.do/t/topic/227730/27
259 | *
260 | * 该函数用于从API获取最新汇率并计算与人民币的兑换比率
261 | */
262 | function fetchExchangeRate(isFromUrlLoad = false) {
263 | const currency = document.getElementById('currency').value;
264 | const customRateField = document.getElementById('customRate');
265 |
266 | return fetch(`https://throbbing-sun-9eb6.b7483311.workers.dev`)
267 | .then(response => {
268 | if (!response.ok) {
269 | throw new Error(`HTTP error! 状态: ${response.status}`);
270 | }
271 | return response.json();
272 | })
273 | .then(data => {
274 | const originRate = data.rates[currency];
275 | const targetRate = data.rates.CNY;
276 | const rate = targetRate/originRate;
277 |
278 | const utcDate = new Date(data.timestamp);
279 | const eastEightTime = new Date(utcDate.getTime() + (8 * 60 * 60 * 1000));
280 |
281 | const year = eastEightTime.getUTCFullYear();
282 | const month = String(eastEightTime.getUTCMonth() + 1).padStart(2, '0');
283 | const day = String(eastEightTime.getUTCDate()).padStart(2, '0');
284 | const hours = String(eastEightTime.getUTCHours()).padStart(2, '0');
285 | const minutes = String(eastEightTime.getUTCMinutes()).padStart(2, '0');
286 |
287 | const formattedDate = `${year}/${month}/${day} ${hours}:${minutes}`;
288 |
289 | document.getElementById('exchangeRate').value = rate.toFixed(3);
290 |
291 | const urlParams = new URLSearchParams(window.location.search);
292 | if (!isFromUrlLoad || !urlParams.has('rate')) {
293 | customRateField.value = rate.toFixed(3);
294 | }
295 |
296 | const exchangeRateField = document.getElementById('exchangeRate');
297 | exchangeRateField.setAttribute('supporting-text', `更新时间: ${formattedDate}`);
298 | })
299 | .catch(error => {
300 | console.error('Error fetching the exchange rate:', error);
301 | showNotification('获取汇率失败,请稍后再试。', 'error');
302 | });
303 | }
304 |
305 | function setDefaultTransactionDate() {
306 | const today = new Date();
307 | const year = today.getFullYear();
308 | const month = String(today.getMonth() + 1).padStart(2, '0');
309 | const day = String(today.getDate()).padStart(2, '0');
310 | const defaultDate = `${year}-${month}-${day}`;
311 | document.getElementById('transactionDate').value = defaultDate;
312 | if (document.getElementById('transactionDate')._flatpickr) {
313 | document.getElementById('transactionDate')._flatpickr.setDate(defaultDate);
314 | }
315 | }
316 |
317 | function calculateRemainingDays(expiryDate, transactionDate) {
318 | const expiry = new Date(expiryDate);
319 | const transaction = new Date(transactionDate);
320 |
321 | // 设置所有时间为当天的开始(00:00:00)
322 | expiry.setHours(0, 0, 0, 0);
323 | transaction.setHours(0, 0, 0, 0);
324 |
325 | // 如果到期日早于或等于交易日期,返回0
326 | if (expiry <= transaction) {
327 | return 0;
328 | }
329 |
330 | // 计算天数差异
331 | const timeDiff = expiry.getTime() - transaction.getTime();
332 | const daysDiff = Math.floor(timeDiff / (1000 * 3600 * 24));
333 |
334 | return daysDiff;
335 | }
336 |
337 | function getCycleStartDate(expiryDateStr, cycleMonths) {
338 | const end = new Date(expiryDateStr);
339 | const start = new Date(end);
340 | start.setMonth(start.getMonth() - cycleMonths);
341 |
342 | if (start.getDate() !== end.getDate()) {
343 | start.setDate(0);
344 | }
345 | return start;
346 | }
347 |
348 | function calculateAndSend() {
349 | const customRate = parseFloat(document.getElementById('customRate').value);
350 | const amount = parseFloat(document.getElementById('amount').value);
351 | const cycle = parseInt(document.getElementById('cycle').value); // 1,3,6,12...
352 | const expiryDate = document.getElementById('expiryDate').value; // yyyy-mm-dd
353 | const transactionDate = document.getElementById('transactionDate').value;
354 |
355 | if (!(customRate && amount && cycle && expiryDate && transactionDate)) {
356 | showNotification('请填写所有字段并确保输入有效', 'error');
357 | return;
358 | }
359 |
360 |
361 | const localAmount = amount * customRate;
362 |
363 | // 整个计费周期的天数
364 | const cycleStart = getCycleStartDate(expiryDate, cycle);
365 | const totalCycleDays = calculateRemainingDays(expiryDate, cycleStart.toISOString().slice(0,10));
366 |
367 | // 当前剩余天数
368 | const remainingDays = calculateRemainingDays(expiryDate, transactionDate);
369 |
370 | // 真实日费 & 剩余价值
371 | const dailyValue = localAmount / totalCycleDays;
372 | const remainingValue = (dailyValue * remainingDays).toFixed(2);
373 |
374 | const data = {
375 | price: localAmount,
376 | time: remainingDays,
377 | customRate,
378 | amount,
379 | cycle,
380 | expiryDate,
381 | transactionDate,
382 | bidAmount: 0
383 | };
384 | updateResults({ remainingValue }, data);
385 | showNotification('计算完成!', 'success');
386 |
387 | if (parseFloat(remainingValue) >= 1000) {
388 | triggerConfetti();
389 | }
390 | }
391 |
392 |
393 | function updateResults(result, data) {
394 | document.getElementById('resultDate').innerText = data.transactionDate;
395 | document.getElementById('resultForeignRate').innerText = data.customRate.toFixed(3);
396 |
397 | // 计算年化价格
398 | const price = parseFloat(data.price);
399 | const cycleText = getCycleText(data.cycle);
400 | document.getElementById('resultPrice').innerText = `${price.toFixed(2)} 人民币/${cycleText}`;
401 |
402 | document.getElementById('resultDays').innerText = data.time;
403 | document.getElementById('resultExpiry').innerText = data.expiryDate;
404 |
405 | const resultValueElement = document.getElementById('resultValue');
406 | let copyIcon = document.createElement('i');
407 | copyIcon.className = 'fas fa-copy copy-icon';
408 | copyIcon.title = '复制到剪贴板';
409 |
410 | resultValueElement.innerHTML = '';
411 | resultValueElement.appendChild(document.createTextNode(`${result.remainingValue} 元 `));
412 | resultValueElement.appendChild(copyIcon);
413 |
414 | if (parseFloat(result.remainingValue) >= 1000) {
415 | resultValueElement.classList.add('high-value-result');
416 | } else {
417 | resultValueElement.classList.remove('high-value-result');
418 | }
419 |
420 | resultValueElement.style.cursor = 'pointer';
421 |
422 | resultValueElement.addEventListener('click', function() {
423 | copyToClipboard(result.remainingValue);
424 | });
425 |
426 | copyIcon.addEventListener('click', function(e) {
427 | e.stopPropagation();
428 | copyToClipboard(result.remainingValue);
429 | });
430 |
431 | document.getElementById('calcResult').scrollIntoView({ behavior: 'smooth' });
432 | }
433 |
434 | function copyToClipboard(text) {
435 | // 使用现代 Clipboard API
436 | if (navigator.clipboard && window.isSecureContext) {
437 | navigator.clipboard.writeText(text).then(() => {
438 | showNotification('已复制到剪贴板!', 'success');
439 | }).catch(() => {
440 | // 回退到传统方法
441 | fallbackCopyToClipboard(text);
442 | });
443 | } else {
444 | // 回退到传统方法
445 | fallbackCopyToClipboard(text);
446 | }
447 | }
448 |
449 | function fallbackCopyToClipboard(text) {
450 | const textarea = document.createElement('textarea');
451 | textarea.value = text;
452 | textarea.setAttribute('readonly', '');
453 | textarea.style.position = 'absolute';
454 | textarea.style.left = '-9999px';
455 | document.body.appendChild(textarea);
456 |
457 | textarea.select();
458 | try {
459 | document.execCommand('copy');
460 | showNotification('已复制到剪贴板!', 'success');
461 | } catch (err) {
462 | showNotification('复制失败,请手动复制', 'error');
463 | }
464 |
465 | document.body.removeChild(textarea);
466 | }
467 |
468 | function showNotification(message, type) {
469 | const notifications = document.getElementById('notifications') || createNotificationsContainer();
470 |
471 | const notification = document.createElement('div');
472 | notification.className = `notification ${type}`;
473 | notification.textContent = message;
474 |
475 | if (notifications.firstChild) {
476 | notifications.insertBefore(notification, notifications.firstChild);
477 | } else {
478 | notifications.appendChild(notification);
479 | }
480 |
481 | setTimeout(() => {
482 | notification.classList.add('show');
483 | }, 10);
484 |
485 | setTimeout(() => {
486 | notification.classList.remove('show');
487 |
488 | setTimeout(() => {
489 | notification.remove();
490 |
491 | if (notifications.children.length === 0) {
492 | notifications.remove();
493 | }
494 | }, 300);
495 | }, 3000);
496 | }
497 |
498 | function createNotificationsContainer() {
499 | const container = document.createElement('div');
500 | container.id = 'notifications';
501 | document.body.appendChild(container);
502 | return container;
503 | }
504 |
505 |
506 | /**
507 | * 捕获计算结果并上传到图床
508 | */
509 | function captureAndUpload() {
510 | // 检查是否有计算结果
511 | const resultValue = document.getElementById('resultValue');
512 | if (resultValue.textContent.trim() === '0.000 元') {
513 | showNotification('请先计算剩余价值再截图', 'error');
514 | return;
515 | }
516 |
517 | // 显示加载中通知
518 | showNotification('正在生成截图...', 'info');
519 |
520 | // 使用 html2canvas 捕获结果区域
521 | html2canvas(document.getElementById('calcResult'), {
522 | backgroundColor: getComputedStyle(document.documentElement).getPropertyValue('--card-background-color'),
523 | scale: 2, // 使用2倍缩放以获得更清晰的图像
524 | logging: false,
525 | useCORS: true
526 | }).then(function(canvas) {
527 | showNotification('截图生成成功,正在上传...', 'info');
528 |
529 | // 转换为 base64 数据 URL
530 | const imageData = canvas.toDataURL('image/png');
531 |
532 | // 上传到选定的图床
533 | uploadImage(imageData);
534 | }).catch(function(error) {
535 | console.error('截图生成失败:', error);
536 | showNotification('截图生成失败,请重试', 'error');
537 | });
538 | }
539 |
540 | /**
541 | * 将图片上传到配置的图床
542 | * @param {string} imageData - base64 格式的图像数据
543 | */
544 | function uploadImage(imageData) {
545 | // 从 base64 数据创建 Blob
546 | const byteString = atob(imageData.split(',')[1]);
547 | const mimeType = imageData.split(',')[0].split(':')[1].split(';')[0];
548 | const ab = new ArrayBuffer(byteString.length);
549 | const ia = new Uint8Array(ab);
550 |
551 | for (let i = 0; i < byteString.length; i++) {
552 | ia[i] = byteString.charCodeAt(i);
553 | }
554 |
555 | const blob = new Blob([ab], {type: mimeType});
556 | const file = new File([blob], "calculator-result.png", {type: mimeType});
557 |
558 | // 根据图床类型选择不同的上传方法
559 | switch(imgHost.type) {
560 | case 'LskyPro':
561 | uploadToLskyPro(file);
562 | break;
563 | case 'EasyImages':
564 | uploadToEasyImages(file);
565 | break;
566 | default:
567 | showNotification(`不支持的图床类型: ${imgHost.type},请设置为 LskyPro 或 EasyImages`, 'error');
568 | }
569 | }
570 |
571 | /**
572 | * 上传到 LskyPro 图床
573 | * 代码参考: https://greasyfork.org/zh-CN/scripts/487553-nodeseek-%E7%BC%96%E8%BE%91%E5%99%A8%E5%A2%9E%E5%BC%BA
574 | *
575 | * @param {File} file - 要上传的文件
576 | */
577 | function uploadToLskyPro(file) {
578 | const formData = new FormData();
579 | formData.append('file', file);
580 |
581 | const headers = {
582 | 'Accept': 'application/json'
583 | };
584 |
585 | if (imgHost.token) {
586 | headers['Authorization'] = `Bearer ${imgHost.token}`;
587 | }
588 |
589 | fetch(`${imgHost.url}/api/v1/upload`, {
590 | method: 'POST',
591 | headers: headers,
592 | body: formData
593 | })
594 | .then(response => {
595 | if (!response.ok) {
596 | throw new Error(`HTTP error! Status: ${response.status}`);
597 | }
598 | return response.json();
599 | })
600 | .then(data => {
601 | if (data.status === true && data.data && data.data.links) {
602 | // 获取图片URL
603 | const imageUrl = data.data.links.url;
604 | let clipboardText = imageUrl;
605 |
606 | // 如果设置为Markdown格式,则生成Markdown格式的文本
607 | if (imgHost.copyFormat === 'markdown') {
608 | clipboardText = ``;
609 | }
610 |
611 | // 复制到剪贴板
612 | copyToClipboard(clipboardText);
613 |
614 | // 显示通知,指明使用了哪种格式
615 | const formatText = imgHost.copyFormat === 'markdown' ? 'Markdown格式' : '链接';
616 | showNotification(`截图上传成功,${formatText}已复制到剪贴板!`, 'success');
617 | } else {
618 | showNotification('图片上传失败', 'error');
619 | console.error('上传响应异常:', data);
620 | }
621 | })
622 | .catch(error => {
623 | console.error('上传图片失败:', error);
624 | showNotification('上传图片失败,请重试', 'error');
625 | });
626 | }
627 |
628 | /**
629 | * 上传到 EasyImages 图床
630 | * 代码参考: https://greasyfork.org/zh-CN/scripts/487553-nodeseek-%E7%BC%96%E8%BE%91%E5%99%A8%E5%A2%9E%E5%BC%BA
631 | *
632 | * @param {File} file - 要上传的文件
633 | */
634 | function uploadToEasyImages(file) {
635 | const formData = new FormData();
636 | let url = imgHost.url;
637 |
638 | if (imgHost.token) {
639 | // 使用后端API
640 | url += '/api/index.php';
641 | formData.append('token', imgHost.token);
642 | formData.append('image', file);
643 | } else {
644 | // 使用前端API
645 | url += '/app/upload.php';
646 | formData.append('file', file);
647 | formData.append('sign', Math.floor(Date.now() / 1000));
648 | }
649 |
650 | fetch(url, {
651 | method: 'POST',
652 | body: formData
653 | })
654 | .then(response => {
655 | if (!response.ok) {
656 | throw new Error(`HTTP error! Status: ${response.status}`);
657 | }
658 | return response.json();
659 | })
660 | .then(data => {
661 | if (data.code === 200 && data.url) {
662 | // 获取图片URL
663 | const imageUrl = data.url;
664 | let clipboardText = imageUrl;
665 |
666 | // 如果设置为Markdown格式,则生成Markdown格式的文本
667 | if (imgHost.copyFormat === 'markdown') {
668 | clipboardText = ``;
669 | }
670 |
671 | // 复制到剪贴板
672 | copyToClipboard(clipboardText);
673 |
674 | // 显示通知,指明使用了哪种格式
675 | const formatText = imgHost.copyFormat === 'markdown' ? 'Markdown格式' : '链接';
676 | showNotification(`截图上传成功,${formatText}已复制到剪贴板!`, 'success');
677 | } else {
678 | showNotification('图片上传失败', 'error');
679 | console.error('上传响应异常:', data);
680 | }
681 | })
682 | .catch(error => {
683 | console.error('上传图片失败:', error);
684 | showNotification('上传图片失败,请重试', 'error');
685 | });
686 | }
687 |
688 |
689 |
690 |
691 | /**
692 | * 初始化设置界面
693 | */
694 | function initSettings() {
695 | const savedSettings = localStorage.getItem('imgHostSettings');
696 |
697 | if (savedSettings) {
698 | // 不是第一次启动,加载保存的设置
699 | const parsedSettings = JSON.parse(savedSettings);
700 |
701 | imgHost.type = parsedSettings.type || imgHost.type;
702 | imgHost.url = parsedSettings.url || imgHost.url;
703 | imgHost.token = parsedSettings.token || imgHost.token;
704 | imgHost.copyFormat = parsedSettings.copyFormat || imgHost.copyFormat;
705 |
706 | document.getElementById('imgHostType').value = imgHost.type;
707 | document.getElementById('imgHostUrl').value = imgHost.url;
708 | document.getElementById('imgHostToken').value = imgHost.token || '';
709 |
710 | if (imgHost.copyFormat === 'markdown') {
711 | document.getElementById('copyFormatMarkdown').checked = true;
712 | } else {
713 | document.getElementById('copyFormatUrl').checked = true;
714 | }
715 |
716 | } else {
717 |
718 | // 也可以在这里设置默认值到UI
719 | document.getElementById('imgHostType').value = imgHost.type;
720 | document.getElementById('imgHostUrl').value = imgHost.url;
721 | document.getElementById('imgHostToken').value = '';
722 |
723 | if (imgHost.copyFormat === 'markdown') {
724 | document.getElementById('copyFormatMarkdown').checked = true;
725 | } else {
726 | document.getElementById('copyFormatUrl').checked = true;
727 | }
728 | }
729 | }
730 |
731 | /**
732 | * 打开设置侧边栏
733 | */
734 | function openSettingsSidebar() {
735 | const sidebar = document.getElementById('settingsSidebar');
736 | const overlay = document.getElementById('sidebarOverlay');
737 |
738 | sidebar.classList.add('active');
739 | overlay.classList.add('active');
740 |
741 | // 防止背景滚动
742 | document.body.style.overflow = 'hidden';
743 | }
744 |
745 | /**
746 | * 关闭设置侧边栏
747 | */
748 | function closeSettingsSidebar() {
749 | const sidebar = document.getElementById('settingsSidebar');
750 | const overlay = document.getElementById('sidebarOverlay');
751 |
752 | sidebar.classList.remove('active');
753 | overlay.classList.remove('active');
754 |
755 | // 恢复背景滚动
756 | document.body.style.overflow = '';
757 | }
758 |
759 | /**
760 | * 保存设置 - 适配Material Web组件
761 | */
762 | function saveSettings() {
763 | const type = document.getElementById('imgHostType').value;
764 | const url = document.getElementById('imgHostUrl').value;
765 | const token = document.getElementById('imgHostToken').value;
766 |
767 | // 获取选中的复制格式 - 适配Material Web md-radio组件
768 | let copyFormat = 'markdown';
769 | const markdownRadio = document.getElementById('copyFormatMarkdown');
770 | const urlRadio = document.getElementById('copyFormatUrl');
771 |
772 | if (markdownRadio && markdownRadio.checked) {
773 | copyFormat = 'markdown';
774 | } else if (urlRadio && urlRadio.checked) {
775 | copyFormat = 'url';
776 | }
777 |
778 | if (!url) {
779 | showNotification('图床地址不能为空', 'error');
780 | return;
781 | }
782 |
783 | // 确保URL格式正确
784 | if (!url.startsWith('http://') && !url.startsWith('https://')) {
785 | showNotification('图床地址必须包含 http:// 或 https://', 'error');
786 | return;
787 | }
788 |
789 | // 更新imgHost对象 - 使用对象属性更新而不是重新赋值
790 | imgHost.type = type;
791 | imgHost.url = url;
792 | imgHost.token = token;
793 | imgHost.copyFormat = copyFormat;
794 |
795 | try {
796 | localStorage.setItem('imgHostSettings', JSON.stringify(imgHost));
797 | showNotification('设置已保存', 'success');
798 | closeSettingsSidebar();
799 | } catch (error) {
800 | showNotification('设置保存失败,可能是浏览器限制', 'error');
801 | }
802 | }
803 |
804 |
805 | function resetSettings() {
806 | if (confirm('确定要恢复默认设置吗?')) {
807 | // 使用对象属性更新
808 | imgHost.type = "LskyPro";
809 | imgHost.url = "https://image.dooo.ng";
810 | imgHost.token = "";
811 | imgHost.copyFormat = "markdown";
812 |
813 | // 更新表单值
814 | document.getElementById('imgHostType').value = imgHost.type;
815 | document.getElementById('imgHostUrl').value = imgHost.url;
816 | document.getElementById('imgHostToken').value = imgHost.token;
817 | document.getElementById('copyFormatMarkdown').checked = true;
818 |
819 | // 保存到本地存储
820 | try {
821 | localStorage.setItem('imgHostSettings', JSON.stringify(imgHost));
822 | showNotification('已恢复默认设置', 'success');
823 | } catch (error) {
824 | showNotification('设置重置失败,可能是浏览器限制', 'error');
825 | }
826 | }
827 | }
828 |
829 |
830 | function togglePasswordVisibility() {
831 | const passwordInput = document.getElementById('imgHostToken');
832 | const toggleBtn = document.querySelector('.toggle-password i');
833 |
834 | if (passwordInput.type === 'password') {
835 | passwordInput.type = 'text';
836 | toggleBtn.className = 'fas fa-eye-slash';
837 | } else {
838 | passwordInput.type = 'password';
839 | toggleBtn.className = 'fas fa-eye';
840 | }
841 | }
842 |
843 |
844 | function triggerConfetti() {
845 | confetti({
846 | particleCount: 15,
847 | angle: 60,
848 | spread: 40,
849 | origin: { x: 0 },
850 | colors: ['#FFD700'],
851 | zIndex: 2000
852 | });
853 |
854 | confetti({
855 | particleCount: 15,
856 | angle: 120,
857 | spread: 40,
858 | origin: { x: 1 },
859 | colors: ['#FFD700'],
860 | zIndex: 2000
861 | });
862 | }
863 |
864 | function getCycleText(cycle) {
865 | switch(parseInt(cycle)) {
866 | case 1: return '月';
867 | case 3: return '季度';
868 | case 6: return '半年';
869 | case 12: return '年';
870 | case 24: return '两年';
871 | case 36: return '三年';
872 | case 48: return '四年';
873 | case 60: return '五年';
874 | default: return '未知周期';
875 | }
876 | }
877 |
878 | function copyLink() {
879 | const currency = document.getElementById('currency').value;
880 | const price = document.getElementById('amount').value;
881 | const cycle = document.getElementById('cycle').value;
882 | const expiryDate = document.getElementById('expiryDate').value;
883 |
884 | const params = new URLSearchParams();
885 | if (currency) params.set('currency', currency);
886 | if (price) params.set('price', price);
887 | if (cycle) params.set('cycle', cycle);
888 | if (expiryDate) params.set('due', expiryDate.replace(/-/g, ''));
889 |
890 | const url = new URL(window.location.href);
891 | url.search = params.toString();
892 |
893 | copyToClipboard(url.toString());
894 | }
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --md-sys-color-primary: #333446;
3 | --md-sys-color-on-primary: #FFFFFF;
4 | --md-sys-color-primary-container: #B8CFCE;
5 | --md-sys-color-on-primary-container:#00131A;
6 |
7 | --md-sys-color-secondary: #7F8CAA;
8 | --md-sys-color-on-secondary: #FFFFFF;
9 | --md-sys-color-secondary-container:#D3D8E8;
10 | --md-sys-color-on-secondary-container:#10131E;
11 |
12 | --md-sys-color-tertiary: #B8CFCE;
13 | --md-sys-color-on-tertiary: #002021;
14 |
15 | --md-sys-color-surface: #FFFFFF;
16 | --md-sys-color-on-surface: #333446;
17 | --md-sys-color-surface-variant: #E0E4E4;
18 | --md-sys-color-on-surface-variant: #505462;
19 | --md-sys-color-surface-container: #EAEFEF;
20 | --md-sys-color-surface-container-high: #F4F7F7;
21 | --md-sys-color-surface-container-highest:#FFFFFF;
22 |
23 | --md-sys-color-background: #EAEFEF;
24 | --md-sys-color-on-background: #333446;
25 |
26 | --md-sys-color-error: #BA1A1A;
27 | --md-sys-color-on-error: #FFFFFF;
28 | --md-sys-color-error-container: #FFDAD6;
29 | --md-sys-color-on-error-container: #410002;
30 |
31 | --md-sys-color-success: #4CAF50;
32 | --md-sys-color-on-success: #FFFFFF;
33 | --md-sys-color-success-container: #C8E6C9;
34 | --md-sys-color-on-success-container:#00390A;
35 |
36 | --md-sys-color-outline: #8C909E;
37 | --md-sys-color-outline-variant: #CCD0D8;
38 | --md-sys-color-shadow: #000000;
39 |
40 | --primary-color: var(--md-sys-color-primary);
41 | --secondary-color: var(--md-sys-color-secondary);
42 | --background-color: var(--md-sys-color-background);
43 | --card-background-color: var(--md-sys-color-surface-container);
44 | --text-color: var(--md-sys-color-on-surface);
45 | --border-color: var(--md-sys-color-outline-variant);
46 | --hover-color: var(--md-sys-color-surface-container-high);
47 | --result-color: var(--md-sys-color-success);
48 | --error-color: var(--md-sys-color-error);
49 | --input-background: var(--md-sys-color-surface-variant);
50 | --shadow-color: rgba(0,0,0,.08);
51 | --shadow-hover-color:rgba(0,0,0,.12);
52 | --footer-color: var(--md-sys-color-on-surface-variant);
53 | --success-text: var(--md-sys-color-on-success);
54 | --error-text: var(--md-sys-color-on-error);
55 | }
56 |
57 | [data-theme="dark"] {
58 | --md-sys-color-primary: #B8CFCE;
59 | --md-sys-color-on-primary: #002021;
60 | --md-sys-color-primary-container: #4A5B63;
61 | --md-sys-color-on-primary-container:#D8EAE9;
62 |
63 | --md-sys-color-secondary: #9BA7C4;
64 | --md-sys-color-on-secondary: #0F1421;
65 | --md-sys-color-secondary-container:#424D68;
66 | --md-sys-color-on-secondary-container:#DEE3F3;
67 |
68 | --md-sys-color-tertiary: #7F8CAA;
69 | --md-sys-color-on-tertiary: #00131E;
70 |
71 | --md-sys-color-surface: #12131A;
72 | --md-sys-color-on-surface: #E3E6EF;
73 | --md-sys-color-surface-variant: #505462;
74 | --md-sys-color-on-surface-variant: #CACDD9;
75 | --md-sys-color-surface-container: #1F2028;
76 | --md-sys-color-surface-container-high: #292B33;
77 | --md-sys-color-surface-container-highest:#33353E;
78 |
79 | --md-sys-color-background: #12131A;
80 | --md-sys-color-on-background: #E3E6EF;
81 |
82 | --md-sys-color-error: #FFB4AB;
83 | --md-sys-color-on-error: #690005;
84 | --md-sys-color-error-container: #93000A;
85 | --md-sys-color-on-error-container: #FFDAD6;
86 |
87 | --md-sys-color-success: #7BDA71;
88 | --md-sys-color-on-success: #00390A;
89 | --md-sys-color-success-container: #005313;
90 | --md-sys-color-on-success-container:#97F68D;
91 |
92 | --md-sys-color-outline: #9599A8;
93 | --md-sys-color-outline-variant: #505462;
94 | --md-sys-color-shadow: #000000;
95 |
96 | --primary-color: var(--md-sys-color-primary);
97 | --secondary-color: var(--md-sys-color-secondary);
98 | --background-color: var(--md-sys-color-background);
99 | --card-background-color: var(--md-sys-color-surface-container);
100 | --text-color: var(--md-sys-color-on-surface);
101 | --border-color: var(--md-sys-color-outline-variant);
102 | --hover-color: var(--md-sys-color-surface-container-high);
103 | --result-color: var(--md-sys-color-success);
104 | --error-color: var(--md-sys-color-error);
105 | --input-background: var(--md-sys-color-surface-variant);
106 | --shadow-color: rgba(0,0,0,.30);
107 | --shadow-hover-color:rgba(0,0,0,.40);
108 | --footer-color: var(--md-sys-color-on-surface-variant);
109 | --success-text: var(--md-sys-color-on-success);
110 | --error-text: var(--md-sys-color-on-error);
111 | }
112 |
113 | .md-elevation--1 {
114 | box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15);
115 | }
116 |
117 | .md-elevation--2 {
118 | box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15);
119 | }
120 |
121 | .md-elevation--3 {
122 | box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.3), 0px 4px 8px 3px rgba(0, 0, 0, 0.15);
123 | }
124 |
125 | .md-elevation--4 {
126 | box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.3), 0px 6px 10px 4px rgba(0, 0, 0, 0.15);
127 | }
128 |
129 | .md-elevation--5 {
130 | box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.3), 0px 8px 12px 6px rgba(0, 0, 0, 0.15);
131 | }
132 |
133 | /* 重置基本样式 */
134 | * {
135 | margin: 0;
136 | padding: 0;
137 | box-sizing: border-box;
138 | }
139 |
140 | /* 基本页面样式 */
141 | body {
142 | font-family: 'Roboto', sans-serif;
143 | background-color: var(--md-sys-color-background);
144 | color: var(--md-sys-color-on-background);
145 | line-height: 1.6;
146 | transition: background-color 0.3s ease, color 0.3s ease;
147 | }
148 |
149 | /* 容器样式 */
150 | .container {
151 | max-width: 1200px;
152 | margin: 0 auto;
153 | padding: 2rem;
154 | position: relative;
155 | }
156 |
157 | /* 页头样式 */
158 | header {
159 | text-align: center;
160 | margin-bottom: 3rem;
161 | position: relative;
162 | }
163 |
164 | h1 {
165 | color: var(--md-sys-color-primary);
166 | display: flex;
167 | align-items: center;
168 | justify-content: center;
169 | gap: 0.5rem;
170 | transition: color 0.3s ease;
171 | }
172 |
173 | h1 i {
174 | font-size: inherit;
175 | }
176 |
177 | /* 主要内容布局 */
178 | main {
179 | display: grid;
180 | grid-template-columns: 1fr 1fr;
181 | gap: 2rem;
182 | }
183 |
184 | /* 计算器和结果区域的共同样式 */
185 | .calculator, .result {
186 | background-color: var(--md-sys-color-surface-container);
187 | border-radius: 24px;
188 | padding: 2rem;
189 | transition: all 0.3s ease, background-color 0.3s ease;
190 | }
191 |
192 | /* 结果区域特定样式 */
193 | .result {
194 | position: sticky;
195 | top: 2rem;
196 | height: fit-content;
197 | }
198 |
199 | /* Material Design 3 悬停效果 */
200 | .calculator:hover, .result:hover {
201 | background-color: var(--md-sys-color-surface-container-high);
202 | }
203 |
204 | /* 输入组样式 */
205 | .input-group {
206 | display: grid;
207 | grid-template-columns: 1fr 1fr;
208 | gap: 1.5rem;
209 | margin-bottom: 1.5rem;
210 | }
211 |
212 | /* 表单组样式 */
213 | .form-group {
214 | display: flex;
215 | flex-direction: column;
216 | position: relative;
217 | }
218 |
219 | /* Material Web组件样式覆盖 */
220 | md-outlined-text-field,
221 | md-outlined-select {
222 | width: 100%;
223 | margin-bottom: 0.5rem;
224 | }
225 |
226 | /* 计算按钮样式 */
227 | .calculate-button {
228 | width: 100%;
229 | margin-top: 1rem;
230 | --md-filled-button-container-height: 56px;
231 | }
232 |
233 | /* 添加鼠标指针样式 */
234 | input[type="text"]#expiryDate,
235 | input[type="text"]#transactionDate {
236 | cursor: pointer;
237 | }
238 |
239 | /* 输入框和选择框焦点样式 */
240 | input:focus, select:focus {
241 | outline: none;
242 | border-color: var(--primary-color);
243 | box-shadow: 0 0 0 3px rgba(108, 92, 231, 0.2);
244 | }
245 |
246 | /* 更新时间样式 */
247 | .update-time {
248 | font-size: 0.75rem;
249 | color: var(--footer-color);
250 | position: absolute;
251 | bottom: -1rem;
252 | left: 0;
253 | transition: color 0.3s ease;
254 | }
255 |
256 | /* 按钮样式 */
257 | button {
258 | width: 100%;
259 | padding: 1rem;
260 | background-color: var(--primary-color);
261 | color: white;
262 | border: none;
263 | border-radius: 12px;
264 | cursor: pointer;
265 | font-size: 1rem;
266 | font-weight: 600;
267 | transition: all 0.3s ease, background-color 0.3s ease;
268 | }
269 |
270 | button:hover {
271 | background-color: var(--secondary-color);
272 | transform: translateY(-2px);
273 | box-shadow: 0 5px 15px rgba(108, 92, 231, 0.4);
274 | }
275 |
276 | button i {
277 | margin-right: 0.5rem;
278 | }
279 |
280 | /* 结果区域标题样式 */
281 | .result-header {
282 | display: flex;
283 | justify-content: space-between;
284 | align-items: center;
285 | margin-bottom: 1.5rem;
286 | }
287 |
288 | .result-header h2 {
289 | color: var(--md-sys-color-primary);
290 | display: flex;
291 | align-items: center;
292 | gap: 0.5rem;
293 | margin: 0;
294 | }
295 |
296 | .result-header h2 i {
297 | font-size: inherit;
298 | }
299 |
300 | /* 结果网格样式 */
301 | .result-grid {
302 | display: grid;
303 | grid-template-columns: 1fr 1fr;
304 | gap: 1rem;
305 | }
306 |
307 | /* 结果项样式 - Material Design Cards */
308 | .result-item {
309 | border-radius: 12px;
310 | transition: all 0.3s ease;
311 | --md-card-container-color: var(--md-sys-color-surface-container-highest);
312 | }
313 |
314 | .result-item:hover {
315 | --md-card-container-color: var(--md-sys-color-surface-container-high);
316 | }
317 |
318 | .result-content {
319 | padding: 1rem;
320 | }
321 |
322 | /* 结果标签样式 */
323 | .result-label {
324 | display: flex;
325 | align-items: center;
326 | gap: 0.5rem;
327 | font-size: 0.875rem;
328 | color: var(--md-sys-color-on-surface-variant);
329 | margin-bottom: 0.5rem;
330 | font-weight: 500;
331 | }
332 |
333 | .result-label i {
334 | font-size: 1.125rem;
335 | color: var(--md-sys-color-primary);
336 | width: 1.125rem;
337 | text-align: center;
338 | }
339 |
340 | /* 结果值样式 */
341 | .result-value {
342 | display: block;
343 | font-weight: 500;
344 | color: var(--md-sys-color-on-surface);
345 | }
346 |
347 | /* 高亮结果项样式 */
348 | .highlight {
349 | --md-card-container-color: var(--md-sys-color-primary-container);
350 | }
351 |
352 | .highlight .result-label {
353 | color: var(--md-sys-color-on-primary-container);
354 | }
355 |
356 | .highlight .result-label i {
357 | color: var(--md-sys-color-on-primary-container);
358 | }
359 |
360 | .highlight .result-value {
361 | color: var(--md-sys-color-on-primary-container);
362 | font-weight: 600;
363 | }
364 |
365 | /* 页脚样式 */
366 | footer {
367 | text-align: center;
368 | margin-top: 2rem;
369 | }
370 |
371 | .footer-card {
372 | --md-card-container-color: var(--md-sys-color-surface-container);
373 | padding: 1.5rem;
374 | display: flex;
375 | justify-content: space-between;
376 | align-items: center;
377 | flex-wrap: wrap;
378 | gap: 1rem;
379 | }
380 |
381 | .project-links {
382 | display: flex;
383 | gap: 1.5rem;
384 | flex-wrap: wrap;
385 | }
386 |
387 | /* 链接样式 */
388 | .project-links a {
389 | color: var(--md-sys-color-primary);
390 | text-decoration: none;
391 | display: flex;
392 | align-items: center;
393 | gap: 0.5rem;
394 | transition: color 0.3s ease;
395 | }
396 |
397 | .project-links a:hover {
398 | color: var(--md-sys-color-secondary);
399 | }
400 |
401 | .project-links a i {
402 | font-size: 1.125rem;
403 | width: 1.125rem;
404 | text-align: center;
405 | }
406 |
407 | .version-badge {
408 | --md-chip-label-text-color: var(--md-sys-color-on-surface-variant);
409 | }
410 |
411 | /* Material Design 3 通知样式 */
412 | #notifications {
413 | position: fixed;
414 | bottom: 20px;
415 | right: 20px;
416 | display: flex;
417 | flex-direction: column-reverse;
418 | gap: 12px;
419 | z-index: 1000;
420 | max-height: 80vh;
421 | overflow-y: auto;
422 | padding-right: 5px;
423 | }
424 |
425 | .notification {
426 | padding: 12px 16px;
427 | border-radius: 12px;
428 | font-weight: 500;
429 | opacity: 0;
430 | transition: all 0.3s ease;
431 | min-width: 200px;
432 | max-width: 320px;
433 | font-size: 0.875rem;
434 | line-height: 1.25rem;
435 | }
436 |
437 | .notification.show {
438 | opacity: 1;
439 | transform: translateY(0);
440 | }
441 |
442 | .notification.success {
443 | background-color: var(--md-sys-color-success-container);
444 | color: var(--md-sys-color-on-success-container);
445 | box-shadow: var(--md-elevation--2);
446 | }
447 |
448 | .notification.error {
449 | background-color: var(--md-sys-color-error-container);
450 | color: var(--md-sys-color-on-error-container);
451 | box-shadow: var(--md-elevation--2);
452 | }
453 |
454 | .notification.warning {
455 | background-color: #fef7e0;
456 | color: #7c2d12;
457 | box-shadow: var(--md-elevation--2);
458 | }
459 |
460 | [data-theme="dark"] .notification.warning {
461 | background-color: #451a03;
462 | color: #fed7aa;
463 | }
464 |
465 | @media (max-width: 768px) {
466 | #notifications {
467 | bottom: 10px;
468 | right: 10px;
469 | left: 10px;
470 | align-items: center;
471 | }
472 |
473 | .notification {
474 | width: 100%;
475 | max-width: 100%;
476 | text-align: center;
477 | font-size: 0.9rem;
478 | padding: 12px;
479 | }
480 | }
481 |
482 | /* Flatpickr 暗黑模式 */
483 | [data-theme="dark"] .flatpickr-calendar {
484 | background: var(--card-background-color);
485 | color: var(--text-color);
486 | box-shadow: 0 3px 13px var(--shadow-color);
487 | border-color: var(--border-color);
488 | }
489 |
490 | [data-theme="dark"] .flatpickr-months {
491 | background-color: var(--card-background-color);
492 | }
493 |
494 | [data-theme="dark"] .flatpickr-month {
495 | color: var(--text-color);
496 | fill: var(--text-color);
497 | }
498 |
499 | [data-theme="dark"] .flatpickr-weekday {
500 | color: var(--text-color);
501 | }
502 |
503 | [data-theme="dark"] .flatpickr-day {
504 | color: var(--text-color);
505 | }
506 |
507 | [data-theme="dark"] .flatpickr-day.selected {
508 | background-color: var(--primary-color);
509 | color: white;
510 | }
511 |
512 | [data-theme="dark"] .flatpickr-day:hover {
513 | background-color: var(--hover-color);
514 | }
515 |
516 | [data-theme="dark"] .flatpickr-day.prevMonthDay,
517 | [data-theme="dark"] .flatpickr-day.nextMonthDay {
518 | color: var(--footer-color);
519 | }
520 |
521 | [data-theme="dark"] .flatpickr-current-month .flatpickr-monthDropdown-months {
522 | color: var(--text-color);
523 | background-color: var(--card-background-color);
524 | }
525 |
526 | /* Flatpickr 年份选择器箭头按钮暗色模式样式 */
527 | [data-theme="dark"] .flatpickr-prev-month,
528 | [data-theme="dark"] .flatpickr-next-month {
529 | color: var(--text-color) !important;
530 | fill: var(--text-color) !important;
531 | }
532 |
533 | [data-theme="dark"] .flatpickr-prev-month:hover,
534 | [data-theme="dark"] .flatpickr-next-month:hover {
535 | color: var(--primary-color) !important;
536 | fill: var(--primary-color) !important;
537 | }
538 |
539 | /* 年份选择器的上下箭头 - 正确的类名和属性 */
540 | [data-theme="dark"] .numInputWrapper span.arrowUp:after {
541 | border-bottom-color: var(--text-color) !important;
542 | }
543 |
544 | [data-theme="dark"] .numInputWrapper span.arrowDown:after {
545 | border-top-color: var(--text-color) !important;
546 | }
547 |
548 | [data-theme="dark"] .numInputWrapper span.arrowUp:hover:after {
549 | border-bottom-color: var(--primary-color) !important;
550 | }
551 |
552 | [data-theme="dark"] .numInputWrapper span.arrowDown:hover:after {
553 | border-top-color: var(--primary-color) !important;
554 | }
555 |
556 | /* 年份选择器箭头按钮背景 */
557 | [data-theme="dark"] .numInputWrapper span {
558 | border-color: var(--border-color) !important;
559 | background-color: var(--card-background-color) !important;
560 | }
561 |
562 | [data-theme="dark"] .numInputWrapper span:hover {
563 | background-color: var(--hover-color) !important;
564 | }
565 |
566 | [data-theme="dark"] .numInputWrapper:hover {
567 | background-color: var(--hover-color) !important;
568 | }
569 |
570 | /* 确保在Flatpickr日历容器内的年份选择器箭头也被正确样式化 */
571 | [data-theme="dark"] .flatpickr-calendar .numInputWrapper span.arrowUp:after {
572 | border-bottom-color: var(--text-color) !important;
573 | }
574 |
575 | [data-theme="dark"] .flatpickr-calendar .numInputWrapper span.arrowDown:after {
576 | border-top-color: var(--text-color) !important;
577 | }
578 |
579 | [data-theme="dark"] .flatpickr-calendar .numInputWrapper span.arrowUp:hover:after {
580 | border-bottom-color: var(--primary-color) !important;
581 | }
582 |
583 | [data-theme="dark"] .flatpickr-calendar .numInputWrapper span.arrowDown:hover:after {
584 | border-top-color: var(--primary-color) !important;
585 | }
586 |
587 | [data-theme="dark"] .flatpickr-calendar .numInputWrapper span {
588 | border-color: var(--border-color) !important;
589 | background-color: var(--card-background-color) !important;
590 | }
591 |
592 | [data-theme="dark"] .flatpickr-calendar .numInputWrapper span:hover {
593 | background-color: var(--hover-color) !important;
594 | }
595 |
596 | /* 年份输入框样式 */
597 | [data-theme="dark"] .flatpickr-current-month .cur-year {
598 | color: var(--text-color);
599 | background-color: var(--card-background-color);
600 | }
601 |
602 | [data-theme="dark"] .flatpickr-current-month .cur-year:hover {
603 | background-color: var(--hover-color);
604 | }
605 |
606 | /* 月份选择器样式 */
607 | [data-theme="dark"] .flatpickr-current-month .flatpickr-monthDropdown-months:hover {
608 | background-color: var(--hover-color);
609 | }
610 |
611 | /* Material Design 3 响应式设计 */
612 | @media (max-width: 768px) {
613 | main {
614 | grid-template-columns: 1fr;
615 | }
616 |
617 | .result {
618 | position: static;
619 | margin-top: 2rem;
620 | }
621 |
622 | .input-group {
623 | grid-template-columns: 1fr;
624 | }
625 |
626 | .theme-toggle {
627 | top: 1rem;
628 | right: 1rem;
629 | }
630 |
631 | .settings-toggle {
632 | top: 1rem;
633 | right: 4rem;
634 | }
635 |
636 | .footer-card {
637 | flex-direction: column;
638 | text-align: center;
639 | }
640 |
641 | .project-links {
642 | justify-content: center;
643 | }
644 |
645 | .result-grid {
646 | grid-template-columns: 1fr;
647 | }
648 | }
649 |
650 | /* 移动端优化 */
651 | @media (max-width: 768px) {
652 | /* 容器padding调整 */
653 | .container {
654 | padding: 1rem;
655 | }
656 |
657 | /* 标题大小调整 */
658 | h1 {
659 | font-size: 1.8rem;
660 | }
661 |
662 | /* 主要内容布局 */
663 | main {
664 | grid-template-columns: 1fr;
665 | gap: 1.5rem;
666 | }
667 |
668 | /* 计算器和结果区域调整 */
669 | .calculator, .result {
670 | padding: 1.5rem;
671 | border-radius: 15px;
672 | }
673 |
674 | /* 输入组样式调整 */
675 | .input-group {
676 | grid-template-columns: 1fr;
677 | gap: 0.75rem;
678 | }
679 |
680 | /* 表单元素大小调整 */
681 | input, select {
682 | padding: 0.8rem;
683 | font-size: 16px;
684 | height: 45px;
685 | }
686 |
687 | /* 按钮样式调整 */
688 | button {
689 | padding: 0.8rem;
690 | height: 45px;
691 | margin-top: 0.5rem;
692 | }
693 |
694 | /* 结果区域调整 */
695 | .result {
696 | margin-top: 1.5rem;
697 | }
698 |
699 | .result h2 {
700 | font-size: 1.5rem;
701 | margin-bottom: 1rem;
702 | }
703 |
704 | /* 结果网格调整为单列 */
705 | .result-grid {
706 | grid-template-columns: 1fr;
707 | gap: 1rem;
708 | }
709 |
710 | /* 结果项样式调整 */
711 | .result-item {
712 | padding: 0.8rem;
713 | }
714 |
715 | .result-label {
716 | font-size: 0.85rem;
717 | }
718 |
719 | .result-value {
720 | font-size: 1.1rem;
721 | }
722 |
723 | /* 高亮结果项调整 */
724 | .highlight .result-value {
725 | font-size: 1.3rem;
726 | }
727 | }
728 |
729 | @media (max-width: 768px) {
730 | button,
731 | select,
732 | input[type="text"],
733 | .result-item {
734 | cursor: pointer;
735 | -webkit-tap-highlight-color: transparent;
736 | }
737 |
738 | /* 优化触摸反馈 */
739 | button:active,
740 | .result-item:active {
741 | transform: scale(0.98);
742 | }
743 |
744 | /* 调整通知位置和大小 */
745 | .notification {
746 | bottom: 10px;
747 | left: 10px;
748 | right: 10px;
749 | text-align: center;
750 | font-size: 0.9rem;
751 | padding: 12px;
752 | }
753 | }
754 |
755 | /* 针对特小屏幕的额外优化 */
756 | @media (max-width: 320px) {
757 | .container {
758 | padding: 0.8rem;
759 | }
760 |
761 | h1 {
762 | font-size: 1.5rem;
763 | }
764 |
765 | .calculator, .result {
766 | padding: 1rem;
767 | }
768 |
769 | input, select, button {
770 | font-size: 14px;
771 | }
772 | }
773 |
774 | /* 加入鼠标选中和点击效果 */
775 | .highlight .result-value {
776 | color: var(--md-sys-color-on-primary-container);
777 | font-size: 1.5rem;
778 | cursor: pointer;
779 | transition: all 0.3s ease;
780 | display: flex;
781 | align-items: center;
782 | gap: 0.5rem;
783 | }
784 |
785 |
786 | .copy-icon {
787 | color: var(--md-sys-color-on-primary-container);
788 | font-size: 1rem;
789 | cursor: pointer;
790 | transition: all 0.3s ease;
791 | padding: 0.25rem;
792 | background-color: rgba(255, 255, 255, 0.1);
793 | border-radius: 8px;
794 | opacity: 0.8;
795 | }
796 |
797 | .copy-icon:hover {
798 | background-color: rgba(255, 255, 255, 0.2);
799 | transform: scale(1.1);
800 | opacity: 1;
801 | }
802 |
803 | .highlight .result-value:active,
804 | .copy-icon:active {
805 | transform: scale(0.95);
806 | }
807 |
808 |
809 | /* 结果区域标题栏样式 */
810 | .result-header {
811 | display: flex;
812 | justify-content: space-between;
813 | align-items: center;
814 | margin-bottom: 1.5rem;
815 | position: relative;
816 | }
817 |
818 | /* 截图按钮样式 */
819 | .screenshot-btn {
820 | width: 30px;
821 | height: 30px;
822 | border-radius: 50%;
823 | background-color: transparent;
824 | color: var(--primary-color);
825 | border: none;
826 | cursor: pointer;
827 | display: flex;
828 | justify-content: center;
829 | align-items: center;
830 | transition: all 0.3s ease;
831 | padding: 0;
832 | box-shadow: none;
833 | position: absolute;
834 | right: 35px;
835 | top: 38px;
836 | line-height: 1;
837 | }
838 |
839 | .screenshot-btn:hover {
840 | background-color: transparent;
841 | transform: scale(1.1);
842 | box-shadow: none;
843 | color: var(--secondary-color);
844 | }
845 |
846 | .screenshot-btn i {
847 | font-size: 1.2rem;
848 | display: block;
849 | margin: 0;
850 | padding: 0;
851 | }
852 |
853 |
854 | /* 设置侧边栏样式 */
855 | .sidebar-overlay {
856 | position: fixed;
857 | top: 0;
858 | left: 0;
859 | width: 100%;
860 | height: 100%;
861 | background-color: rgba(0, 0, 0, 0.5);
862 | z-index: 999;
863 | opacity: 0;
864 | visibility: hidden;
865 | transition: opacity 0.3s ease, visibility 0.3s ease;
866 | }
867 |
868 | .sidebar-overlay.active {
869 | opacity: 1;
870 | visibility: visible;
871 | }
872 |
873 | .settings-sidebar {
874 | position: fixed;
875 | top: 0;
876 | right: -400px;
877 | width: 400px;
878 | height: 100%;
879 | z-index: 1000;
880 | transition: right 0.3s ease;
881 | }
882 |
883 | .settings-sidebar.active {
884 | right: 0;
885 | }
886 |
887 | .sidebar-card {
888 | height: 100%;
889 | border-radius: 0;
890 | display: flex;
891 | flex-direction: column;
892 | background-color: var(--md-sys-color-surface-container-high);
893 | --md-elevated-card-container-color: var(--md-sys-color-surface-container-high);
894 | }
895 |
896 | .sidebar-header {
897 | display: flex;
898 | justify-content: space-between;
899 | align-items: center;
900 | padding: 1.5rem;
901 | border-bottom: 1px solid var(--md-sys-color-outline-variant);
902 | flex-shrink: 0;
903 | }
904 |
905 | .sidebar-header h2 {
906 | margin: 0;
907 | color: var(--md-sys-color-on-surface);
908 | display: flex;
909 | align-items: center;
910 | gap: 0.5rem;
911 | }
912 |
913 | .sidebar-header h2 i {
914 | color: var(--md-sys-color-primary);
915 | }
916 |
917 | .sidebar-content {
918 | flex: 1;
919 | padding: 1.5rem;
920 | overflow-y: auto;
921 | }
922 |
923 | .sidebar-actions {
924 | padding: 1rem 1.5rem;
925 | border-top: 1px solid var(--md-sys-color-outline-variant);
926 | display: flex;
927 | justify-content: flex-end;
928 | gap: 1rem;
929 | flex-shrink: 0;
930 | }
931 |
932 | /* 设置表单样式 */
933 | .sidebar-content .form-group {
934 | margin-bottom: 1.5rem;
935 | }
936 |
937 | .radio-group-label {
938 | display: flex;
939 | align-items: center;
940 | gap: 0.5rem;
941 | font-size: 0.875rem;
942 | color: var(--md-sys-color-on-surface-variant);
943 | margin-bottom: 0.75rem;
944 | font-weight: 500;
945 | }
946 |
947 | .radio-group-label i {
948 | font-size: 1.125rem;
949 | color: var(--md-sys-color-primary);
950 | }
951 |
952 | .radio-group {
953 | display: flex;
954 | gap: 1.5rem;
955 | align-items: center;
956 | margin-bottom: 0.5rem;
957 | }
958 |
959 | .radio-label {
960 | font-size: 0.875rem;
961 | color: var(--md-sys-color-on-surface);
962 | margin-left: 0.5rem;
963 | cursor: pointer;
964 | }
965 |
966 | .help-text {
967 | font-size: 0.75rem;
968 | color: var(--md-sys-color-on-surface-variant);
969 | margin-top: 0.25rem;
970 | }
971 |
972 |
973 | /* 输入框带切换按钮 */
974 | .input-with-toggle {
975 | position: relative;
976 | display: flex;
977 | }
978 |
979 | .input-with-toggle input {
980 | flex: 1;
981 | padding-right: 40px;
982 | }
983 |
984 | .toggle-password {
985 | position: absolute;
986 | right: 0;
987 | top: 0;
988 | height: 100%;
989 | width: 40px;
990 | background: transparent;
991 | border: none;
992 | color: var(--text-color);
993 | cursor: pointer;
994 | display: flex;
995 | justify-content: center;
996 | align-items: center;
997 | transition: all 0.3s ease;
998 | box-shadow: none;
999 | }
1000 |
1001 | .toggle-password:hover {
1002 | color: var(--primary-color);
1003 | background: transparent;
1004 | transform: none;
1005 | box-shadow: none;
1006 | }
1007 |
1008 | /* 帮助文本样式 */
1009 | .help-text {
1010 | font-size: 0.75rem;
1011 | color: var(--footer-color);
1012 | margin-top: 0.3rem;
1013 | }
1014 |
1015 | /* 单选按钮组样式 */
1016 | .radio-group {
1017 | display: flex;
1018 | gap: 1.5rem;
1019 | margin: 0.5rem 0;
1020 | }
1021 |
1022 | .radio-label {
1023 | display: flex;
1024 | align-items: center;
1025 | cursor: pointer;
1026 | font-weight: normal;
1027 | margin-bottom: 0;
1028 | }
1029 |
1030 | .radio-label input[type="radio"] {
1031 | width: auto;
1032 | margin: 0 0.5rem 0 0;
1033 | cursor: pointer;
1034 | appearance: none;
1035 | -webkit-appearance: none;
1036 | border: 2px solid var(--border-color);
1037 | border-radius: 50%;
1038 | width: 18px;
1039 | height: 18px;
1040 | position: relative;
1041 | transition: all 0.2s ease;
1042 | }
1043 |
1044 | .radio-label input[type="radio"]:checked {
1045 | border-color: var(--primary-color);
1046 | background-color: var(--primary-color);
1047 | box-shadow: inset 0 0 0 4px var(--card-background-color);
1048 | }
1049 |
1050 | .radio-label input[type="radio"]:focus {
1051 | outline: none;
1052 | box-shadow: 0 0 0 3px rgba(108, 92, 231, 0.2), inset 0 0 0 4px var(--card-background-color);
1053 | }
1054 |
1055 | .radio-label span {
1056 | font-size: 0.95rem;
1057 | }
1058 |
1059 | /* 按钮样式 */
1060 | .primary-btn, .secondary-btn {
1061 | padding: 0.75rem 1.5rem;
1062 | border-radius: 12px;
1063 | font-weight: 500;
1064 | transition: all 0.3s ease;
1065 | width: auto;
1066 | }
1067 |
1068 | .primary-btn {
1069 | background-color: var(--primary-color);
1070 | color: white;
1071 | }
1072 |
1073 | .secondary-btn {
1074 | background-color: transparent;
1075 | color: var(--text-color);
1076 | border: 1px solid var(--border-color);
1077 | }
1078 |
1079 | .primary-btn:hover {
1080 | background-color: var(--secondary-color);
1081 | }
1082 |
1083 | .secondary-btn:hover {
1084 | background-color: var(--hover-color);
1085 | border-color: var(--text-color);
1086 | }
1087 |
1088 | /* 响应式调整 */
1089 | @media (max-width: 768px) {
1090 | .settings-toggle {
1091 | top: 1rem;
1092 | right: 4rem;
1093 | }
1094 |
1095 | #settingsToggle {
1096 | width: 36px;
1097 | height: 36px;
1098 | }
1099 |
1100 | /* 侧边栏响应式设计 */
1101 | .settings-sidebar {
1102 | width: 100%;
1103 | right: -100%;
1104 | }
1105 |
1106 | .sidebar-header {
1107 | padding: 1rem;
1108 | }
1109 |
1110 | .sidebar-content {
1111 | padding: 1rem;
1112 | }
1113 |
1114 | .sidebar-actions {
1115 | padding: 1rem;
1116 | flex-direction: column;
1117 | gap: 0.5rem;
1118 | }
1119 |
1120 | .sidebar-actions md-filled-button,
1121 | .sidebar-actions md-text-button {
1122 | width: 100%;
1123 | }
1124 | }
1125 |
1126 | /* Material Design 3 高价值结果动画 */
1127 | @keyframes high-value-glow {
1128 | 0% {
1129 | transform: scale(1);
1130 | filter: brightness(1);
1131 | }
1132 | 50% {
1133 | transform: scale(1.05);
1134 | filter: brightness(1.2);
1135 | }
1136 | 100% {
1137 | transform: scale(1);
1138 | filter: brightness(1);
1139 | }
1140 | }
1141 |
1142 | .high-value-result {
1143 | animation: high-value-glow 2s ease-in-out infinite;
1144 | color: var(--md-sys-color-primary) !important;
1145 | font-weight: 700 !important;
1146 | }
1147 |
1148 | /* Material Web 组件自定义样式 */
1149 | md-outlined-text-field {
1150 | --md-outlined-text-field-container-shape: 12px;
1151 | --md-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);
1152 | --md-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);
1153 | }
1154 |
1155 | md-outlined-select {
1156 | --md-outlined-select-text-field-container-shape: 12px;
1157 | --md-outlined-select-text-field-label-text-color: var(--md-sys-color-on-surface-variant);
1158 | }
1159 |
1160 | md-filled-button {
1161 | --md-filled-button-container-shape: 12px;
1162 | }
1163 |
1164 | md-icon-button {
1165 | --md-icon-button-state-layer-shape: 50%;
1166 | }
1167 |
1168 | md-card {
1169 | --md-card-container-shape: 12px;
1170 | }
1171 |
1172 |
1173 | /* Font Awesome 图标在 Material Web 组件中的样式 */
1174 | md-outlined-text-field i[slot="leading-icon"],
1175 | md-outlined-select i[slot="leading-icon"] {
1176 | color: var(--md-sys-color-on-surface-variant);
1177 | font-size: 1.25rem;
1178 | width: 1.25rem;
1179 | text-align: center;
1180 | }
1181 |
1182 | md-filled-button i[slot="icon"],
1183 | md-text-button i[slot="icon"] {
1184 | font-size: 1.125rem;
1185 | width: 1.125rem;
1186 | text-align: center;
1187 | }
1188 |
1189 | md-icon-button i {
1190 | font-size: 1.25rem;
1191 | width: 1.25rem;
1192 | text-align: center;
1193 | color: var(--md-sys-color-on-surface-variant);
1194 | }
1195 |
1196 | [data-theme="dark"] md-outlined-text-field {
1197 | --md-outlined-text-field-outline-color: var(--md-sys-color-outline);
1198 | --md-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);
1199 | --md-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);
1200 | }
1201 |
1202 | [data-theme="dark"] md-outlined-select {
1203 | --md-outlined-select-text-field-outline-color: var(--md-sys-color-outline);
1204 | --md-outlined-select-text-field-label-text-color: var(--md-sys-color-on-surface-variant);
1205 | }
1206 |
1207 | [data-theme="dark"] md-filled-button {
1208 | --md-filled-button-container-color: var(--md-sys-color-primary);
1209 | --md-filled-button-label-text-color: var(--md-sys-color-on-primary);
1210 | }
1211 |
1212 | [data-theme="dark"] md-icon-button {
1213 | --md-icon-button-icon-color: var(--md-sys-color-on-surface-variant);
1214 | }
1215 |
1216 | [data-theme="dark"] md-card {
1217 | --md-card-container-color: var(--md-sys-color-surface-container);
1218 | }
1219 |
1220 | /* 主题切换和设置按钮样式 */
1221 | .theme-toggle, .settings-toggle {
1222 | position: absolute;
1223 | z-index: 100;
1224 | }
1225 |
1226 | .theme-toggle {
1227 | top: 1.5rem;
1228 | right: 1.5rem;
1229 | }
1230 |
1231 | .settings-toggle {
1232 | top: 1.5rem;
1233 | right: 4.5rem;
1234 | }
1235 |
1236 | body.is-loading {
1237 | opacity: 0;
1238 | visibility: hidden;
1239 | transition: none;
1240 | animation: none;
1241 | }
1242 |
1243 | body {
1244 | transition: opacity 0.2s ease-out;
1245 | }
--------------------------------------------------------------------------------
/version.js:
--------------------------------------------------------------------------------
1 | const APP_VERSION = '4.2.0';
2 |
3 | if (typeof window !== 'undefined') {
4 | window.APP_VERSION = APP_VERSION;
5 | }
6 |
7 | document.addEventListener('DOMContentLoaded', function() {
8 | const versionElements = document.querySelectorAll('[data-version]');
9 | versionElements.forEach(element => {
10 | if (element.hasAttribute('href')) {
11 | const url = element.getAttribute('href').split('?')[0];
12 | element.setAttribute('href', `${url}?v=${APP_VERSION}`);
13 | } else if (element.hasAttribute('src')) {
14 | const url = element.getAttribute('src').split('?')[0];
15 | element.setAttribute('src', `${url}?v=${APP_VERSION}`);
16 | } else {
17 | element.textContent = APP_VERSION;
18 | }
19 | });
20 | });
--------------------------------------------------------------------------------