Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9efd9a3099 | ||
|
|
a8ece73038 | ||
|
|
0d5d200844 | ||
|
|
d7b2c5e49a |
@@ -99,12 +99,32 @@ jobs:
|
||||
path: ${{ steps.prepare_apk.outputs.apk_path }}
|
||||
retention-days: 30
|
||||
|
||||
- name: Extract Release Notes from CHANGELOG
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
python3 -c "
|
||||
import re
|
||||
version = '${{ steps.prepare_apk.outputs.version }}'
|
||||
tag = 'v' + version if not version.startswith('v') else version
|
||||
try:
|
||||
with open('CHANGELOG.md', 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
pattern = r'### ' + re.escape(tag) + r'[^\n]*\n(.*?)(?=\n### v|\Z)'
|
||||
m = re.search(pattern, content, re.DOTALL)
|
||||
notes = m.group(1).strip() if m else ''
|
||||
except Exception:
|
||||
notes = ''
|
||||
with open('current_release_notes.md', 'w', encoding='utf-8') as f:
|
||||
f.write(notes)
|
||||
"
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ${{ steps.prepare_apk.outputs.apk_path }}
|
||||
name: Work Hours Tracker v${{ steps.prepare_apk.outputs.version }}
|
||||
body_path: current_release_notes.md
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
name: Sync Release Notes
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'CHANGELOG.md'
|
||||
- '.github/workflows/sync-releases.yml'
|
||||
|
||||
jobs:
|
||||
update-releases:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update GitHub Releases
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
python3 -c "
|
||||
import os, urllib.request, json, re
|
||||
|
||||
token = os.environ['GITHUB_TOKEN']
|
||||
repo = os.environ['REPO']
|
||||
headers = {
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'User-Agent': 'Actions-Release-Syncer'
|
||||
}
|
||||
|
||||
# 1. 解析 CHANGELOG.md 中的各版本内容
|
||||
with open('CHANGELOG.md', 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
pattern = r'### (v\d+\.\d+\.\d+)[^\n]*\n(.*?)(?=\n### v|\Z)'
|
||||
matches = re.findall(pattern, content, re.DOTALL)
|
||||
changelogs = {tag: body.strip() for tag, body in matches}
|
||||
print('Found changelogs for tags:', list(changelogs.keys()))
|
||||
|
||||
# 2. 获取现有的 releases
|
||||
url = f'https://api.github.com/repos/{repo}/releases'
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
releases = json.loads(resp.read().decode())
|
||||
|
||||
# 3. 逐个更新 release body
|
||||
for r in releases:
|
||||
tag = r.get('tag_name')
|
||||
rel_id = r.get('id')
|
||||
if tag in changelogs:
|
||||
new_body = changelogs[tag]
|
||||
# 补全 compare 链接
|
||||
if 'Full Changelog' not in new_body:
|
||||
new_body += f'\n\n---\n**Full Changelog**: https://github.com/{repo}/commits/{tag}'
|
||||
|
||||
update_url = f'https://api.github.com/repos/{repo}/releases/{rel_id}'
|
||||
data = json.dumps({'body': new_body}).encode('utf-8')
|
||||
up_req = urllib.request.Request(update_url, data=data, headers=headers, method='PATCH')
|
||||
with urllib.request.urlopen(up_req) as up_resp:
|
||||
print(f'Successfully updated release {tag} (ID: {rel_id})')
|
||||
else:
|
||||
print(f'No changelog found for tag {tag}')
|
||||
"
|
||||
@@ -0,0 +1,50 @@
|
||||
## 七、 版本发布历史 (Changelog)
|
||||
|
||||
### v1.0.3 (2026-09-16)
|
||||
- **修复 (金额精度保留与无损展示)**:
|
||||
- 去除金额展示层原有的 `toFixed(1)` 进位截断,彻底解决如 `103.5h × ¥19.5/h = ¥2018.25` 在看板与汇总中被四舍五入进位显示为 `¥2018.3` 的问题。
|
||||
- 金额计算与格式化逻辑(`money`、`signed`、`moneyParts`)调整为保留原始数据有效位数,同时消除 JS 浮点二进制精度抖动。
|
||||
- 看板大字整数与小数拆分逻辑自适应原始小数位,保证整数部千分位与小数分位严密吻合。
|
||||
- **Android 原生资产与版本**:
|
||||
- 同步静态资产至 `android/app/src/main/assets/www/`。
|
||||
- 版本号升级:`versionCode 4`,`versionName "1.0.3"`。
|
||||
|
||||
### v1.0.2 (2026-09-15)
|
||||
- **修复 (MCP 状态联动与生命周期)**:
|
||||
- 修复退出登录后前端 `mcp` 响应式对象残留已开启状态、点击关闭弹出「未登录」错误的问题。
|
||||
- 新增 `resetMcpState(authNeeded)` 函数,在 `doLogout` 成功退出时联动重置 MCP 状态并清空掩码。
|
||||
- `loadMcpStatus` 增加登录凭证前置检测与 401/403 异常拦截重置;在 `doLogin` 成功后自动拉取当前账号的专属 MCP 状态。
|
||||
- 未登录时 MCP 接入开关与生成令牌按钮自动置灰禁用,右上角徽章明确显示「本设备请先登录」,`toggleMcp` 增加前置拦截提示。
|
||||
- **Android 原生资产与版本**:
|
||||
- 静态资产同步至 `android/app/src/main/assets/www/`。
|
||||
- 版本号升级:`versionCode 3`,`versionName "1.0.2"`。
|
||||
|
||||
### v1.0.1 (2026-09-15)
|
||||
- **应用名称统一**:
|
||||
- Android 原生应用名称统一由「工时记账」修改为「工时打卡」(`strings.xml`)。
|
||||
- `MainActivity.java` 连续双击返回退出提示文案同步更新为「再按一次退出工时打卡」。
|
||||
- **默认远程服务器探活与回退**:
|
||||
- `sync.js` 预设 `DEFAULT_REMOTE_BASE = 'https://daka.kory.kdns.fr'`。
|
||||
- 深度适配 Android WebView `file:///android_asset/...` 环境,未手动指定服务器地址时自动探活并回退到默认远程服务器。
|
||||
- **设置页交互分流与体验重构**:
|
||||
- 底部导航栏点击「设置」原生切换至 `<section v-show="tab === 'settings'">` 独立全屏视图,保持打卡、汇总、设置三页平级体验。
|
||||
- 顶部导航栏齿轮图标保留快速弹出轻量半屏抽屉(`settingsOpen`)能力,满足快捷调整设置的需求。
|
||||
- **登录 / 注册模式解耦**:
|
||||
- 在设置页「云端同步」卡片中引入「账号登录 / 注册新账号」模式切换(`authMode`)。
|
||||
- 彻底解除了等待服务端异步探测返回 `allowRegister` 按钮才浮现的延迟限制。
|
||||
- **文档与发版**:
|
||||
- 补充 README「六、 Android 客户端与移动端打包」章节。
|
||||
- 版本号升级:`versionCode 2`,`versionName "1.0.1"`。
|
||||
|
||||
### v1.0.0 (2026-09-15)
|
||||
- **Android 原生离线客户端支持**:
|
||||
- 新增基于 Android WebView 的原生包装容器(`android/` 工程)。
|
||||
- 将前端静态资产封装在本地 assets,支持完全脱网离线运行及沉浸式体验。
|
||||
- **构建环境与依赖约束修复**:
|
||||
- 解决 GitHub Actions CI 构建 Release APK 出现的 Kotlin stdlib 多版本冲突(`Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt`)。
|
||||
- 在 `android/app/build.gradle` 引入 Kotlin BOM 平台依赖约束:`org.jetbrains.kotlin:kotlin-bom:1.8.22`。
|
||||
- **CI/CD 自动化构建流水线**:
|
||||
- 新增 `.github/workflows/build-apk.yml` 自动化构建工作流。
|
||||
- 支持推送 `v*` 标签时自动编译、签名并发布 GitHub Release 及附带 APK 校验和。
|
||||
- **系统核心基线**:
|
||||
- Local-First 本地优先架构、纯函数薪酬计算引擎(`shared/payroll.js`)、启动自愈守卫、多用户数据隔离与用户级只读 MCP 服务。
|
||||
@@ -202,3 +202,56 @@ curl -s http://127.0.0.1:9522/api/health
|
||||
- **GitHub Actions 自动化发布**:
|
||||
- 推送带 `v*` 格式的 Git Tag(如 `git tag v1.0.1 && git push github v1.0.1`)时,自动触发 `.github/workflows/build-apk.yml` 工作流。
|
||||
- 构建产物会自动签名并发布到 GitHub Releases,附带安装包与 SHA256 校验和。
|
||||
|
||||
---
|
||||
|
||||
## 七、 版本发布历史 (Changelog)
|
||||
|
||||
### v1.0.3 (2026-09-16)
|
||||
- **修复 (金额精度保留与无损展示)**:
|
||||
- 去除金额展示层原有的 `toFixed(1)` 进位截断,彻底解决如 `103.5h × ¥19.5/h = ¥2018.25` 在看板与汇总中被四舍五入进位显示为 `¥2018.3` 的问题。
|
||||
- 金额计算与格式化逻辑(`money`、`signed`、`moneyParts`)调整为保留原始数据有效位数,同时消除 JS 浮点二进制精度抖动。
|
||||
- 看板大字整数与小数拆分逻辑自适应原始小数位,保证整数部千分位与小数分位严密吻合。
|
||||
- **Android 原生资产与版本**:
|
||||
- 同步静态资产至 `android/app/src/main/assets/www/`。
|
||||
- 版本号升级:`versionCode 4`,`versionName "1.0.3"`。
|
||||
|
||||
### v1.0.2 (2026-09-15)
|
||||
- **修复 (MCP 状态联动与生命周期)**:
|
||||
- 修复退出登录后前端 `mcp` 响应式对象残留已开启状态、点击关闭弹出「未登录」错误的问题。
|
||||
- 新增 `resetMcpState(authNeeded)` 函数,在 `doLogout` 成功退出时联动重置 MCP 状态并清空掩码。
|
||||
- `loadMcpStatus` 增加登录凭证前置检测与 401/403 异常拦截重置;在 `doLogin` 成功后自动拉取当前账号的专属 MCP 状态。
|
||||
- 未登录时 MCP 接入开关与生成令牌按钮自动置灰禁用,右上角徽章明确显示「本设备请先登录」,`toggleMcp` 增加前置拦截提示。
|
||||
- **Android 原生资产与版本**:
|
||||
- 静态资产同步至 `android/app/src/main/assets/www/`。
|
||||
- 版本号升级:`versionCode 3`,`versionName "1.0.2"`。
|
||||
|
||||
### v1.0.1 (2026-09-15)
|
||||
- **应用名称统一**:
|
||||
- Android 原生应用名称统一由「工时记账」修改为「工时打卡」(`strings.xml`)。
|
||||
- `MainActivity.java` 连续双击返回退出提示文案同步更新为「再按一次退出工时打卡」。
|
||||
- **默认远程服务器探活与回退**:
|
||||
- `sync.js` 预设 `DEFAULT_REMOTE_BASE = 'https://daka.kory.kdns.fr'`。
|
||||
- 深度适配 Android WebView `file:///android_asset/...` 环境,未手动指定服务器地址时自动探活并回退到默认远程服务器。
|
||||
- **设置页交互分流与体验重构**:
|
||||
- 底部导航栏点击「设置」原生切换至 `<section v-show="tab === 'settings'">` 独立全屏视图,保持打卡、汇总、设置三页平级体验。
|
||||
- 顶部导航栏齿轮图标保留快速弹出轻量半屏抽屉(`settingsOpen`)能力,满足快捷调整设置的需求。
|
||||
- **登录 / 注册模式解耦**:
|
||||
- 在设置页「云端同步」卡片中引入「账号登录 / 注册新账号」模式切换(`authMode`)。
|
||||
- 彻底解除了等待服务端异步探测返回 `allowRegister` 按钮才浮现的延迟限制。
|
||||
- **文档与发版**:
|
||||
- 补充 README「六、 Android 客户端与移动端打包」章节。
|
||||
- 版本号升级:`versionCode 2`,`versionName "1.0.1"`。
|
||||
|
||||
### v1.0.0 (2026-09-15)
|
||||
- **Android 原生离线客户端支持**:
|
||||
- 新增基于 Android WebView 的原生包装容器(`android/` 工程)。
|
||||
- 将前端静态资产封装在本地 assets,支持完全脱网离线运行及沉浸式体验。
|
||||
- **构建环境与依赖约束修复**:
|
||||
- 解决 GitHub Actions CI 构建 Release APK 出现的 Kotlin stdlib 多版本冲突(`Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt`)。
|
||||
- 在 `android/app/build.gradle` 引入 Kotlin BOM 平台依赖约束:`org.jetbrains.kotlin:kotlin-bom:1.8.22`。
|
||||
- **CI/CD 自动化构建流水线**:
|
||||
- 新增 `.github/workflows/build-apk.yml` 自动化构建工作流。
|
||||
- 支持推送 `v*` 标签时自动编译、签名并发布 GitHub Release 及附带 APK 校验和。
|
||||
- **系统核心基线**:
|
||||
- Local-First 本地优先架构、纯函数薪酬计算引擎(`shared/payroll.js`)、启动自愈守卫、多用户数据隔离与用户级只读 MCP 服务。
|
||||
|
||||
@@ -10,8 +10,8 @@ android {
|
||||
applicationId "top.ouaone.workhours"
|
||||
minSdk 23
|
||||
targetSdk 34
|
||||
versionCode 2
|
||||
versionName "1.0.1"
|
||||
versionCode 4
|
||||
versionName "1.0.3"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
+33
-7
@@ -820,7 +820,7 @@
|
||||
<div class="divide-y divide-slate-100">
|
||||
<div class="flex items-center justify-between px-5 py-3">
|
||||
<span class="text-xs text-slate-600">接入开关</span>
|
||||
<button @click="toggleMcp" :disabled="mcp.busy || !mcp.configured" class="relative w-11 h-6 rounded-full transition shrink-0 disabled:opacity-40" :class="mcp.enabled ? 'bg-emerald-500' : 'bg-slate-300'">
|
||||
<button @click="toggleMcp" :disabled="mcp.busy || !mcp.configured || !sync.user || mcp.authNeeded" class="relative w-11 h-6 rounded-full transition shrink-0 disabled:opacity-40" :class="mcp.enabled ? 'bg-emerald-500' : 'bg-slate-300'">
|
||||
<span class="absolute top-0.5 w-5 h-5 rounded-full bg-white shadow transition-all" :class="mcp.enabled ? 'left-[22px]' : 'left-0.5'"></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -829,7 +829,7 @@
|
||||
<span class="text-[11px] font-bold text-slate-700 font-mono-num">{{ mcp.masked }}</span>
|
||||
</div>
|
||||
<button v-if="mcp.configured" @click="copyMcpToken" :disabled="mcp.busy" class="w-full flex items-center gap-2 px-5 py-3.5 text-xs font-medium text-indigo-600 active:bg-slate-50 transition disabled:opacity-40"><svg class="ico text-[15px]"><use :href="mcpCopied ? '#i-check' : '#i-copy'" /></svg>{{ mcpCopied ? '已复制,去 AI 客户端粘贴' : '复制访问令牌(明文)' }}</button>
|
||||
<button @click="newMcpToken" :disabled="mcp.busy" class="w-full flex items-center gap-2 px-5 py-3.5 text-xs font-medium text-indigo-600 active:bg-slate-50 transition disabled:opacity-40"><svg class="ico text-[15px]" :class="{ 'animate-spin': mcp.busy }"><use href="#i-refresh-cw" /></svg>{{ mcp.configured ? '重置令牌(旧令牌立即失效)' : '生成访问令牌并开启' }}</button>
|
||||
<button @click="newMcpToken" :disabled="mcp.busy || !sync.user" class="w-full flex items-center gap-2 px-5 py-3.5 text-xs font-medium text-indigo-600 active:bg-slate-50 transition disabled:opacity-40"><svg class="ico text-[15px]" :class="{ 'animate-spin': mcp.busy }"><use href="#i-refresh-cw" /></svg>{{ mcp.configured ? '重置令牌(旧令牌立即失效)' : '生成访问令牌并开启' }}</button>
|
||||
</div>
|
||||
<p class="px-5 py-3 text-[10px] text-slate-400 bg-slate-50/70 leading-relaxed">
|
||||
端点 POST /mcp(Bearer 令牌鉴权),只读:AI 只能查询出勤、工时与薪酬,改不了任何数据。开关与令牌改动即时生效,无需重启;重置后旧客户端立即失效。
|
||||
@@ -1038,7 +1038,7 @@
|
||||
<div class="divide-y divide-slate-100">
|
||||
<div class="flex items-center justify-between px-5 py-3">
|
||||
<span class="text-xs text-slate-600">接入开关</span>
|
||||
<button @click="toggleMcp" :disabled="mcp.busy || !mcp.configured" class="relative w-11 h-6 rounded-full transition shrink-0 disabled:opacity-40" :class="mcp.enabled ? 'bg-emerald-500' : 'bg-slate-300'">
|
||||
<button @click="toggleMcp" :disabled="mcp.busy || !mcp.configured || !sync.user || mcp.authNeeded" class="relative w-11 h-6 rounded-full transition shrink-0 disabled:opacity-40" :class="mcp.enabled ? 'bg-emerald-500' : 'bg-slate-300'">
|
||||
<span class="absolute top-0.5 w-5 h-5 rounded-full bg-white shadow transition-all" :class="mcp.enabled ? 'left-[22px]' : 'left-0.5'"></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1047,7 +1047,7 @@
|
||||
<span class="text-[11px] font-bold text-slate-700 font-mono-num">{{ mcp.masked }}</span>
|
||||
</div>
|
||||
<button v-if="mcp.configured" @click="copyMcpToken" :disabled="mcp.busy" class="w-full flex items-center gap-2 px-5 py-3.5 text-xs font-medium text-indigo-600 active:bg-slate-50 transition disabled:opacity-40"><svg class="ico text-[15px]"><use :href="mcpCopied ? '#i-check' : '#i-copy'" /></svg>{{ mcpCopied ? '已复制,去 AI 客户端粘贴' : '复制访问令牌(明文)' }}</button>
|
||||
<button @click="newMcpToken" :disabled="mcp.busy" class="w-full flex items-center gap-2 px-5 py-3.5 text-xs font-medium text-indigo-600 active:bg-slate-50 transition disabled:opacity-40"><svg class="ico text-[15px]" :class="{ 'animate-spin': mcp.busy }"><use href="#i-refresh-cw" /></svg>{{ mcp.configured ? '重置令牌(旧令牌立即失效)' : '生成访问令牌并开启' }}</button>
|
||||
<button @click="newMcpToken" :disabled="mcp.busy || !sync.user" class="w-full flex items-center gap-2 px-5 py-3.5 text-xs font-medium text-indigo-600 active:bg-slate-50 transition disabled:opacity-40"><svg class="ico text-[15px]" :class="{ 'animate-spin': mcp.busy }"><use href="#i-refresh-cw" /></svg>{{ mcp.configured ? '重置令牌(旧令牌立即失效)' : '生成访问令牌并开启' }}</button>
|
||||
</div>
|
||||
<p class="px-5 py-3 text-[10px] text-slate-400 bg-slate-50/70 leading-relaxed">
|
||||
端点 POST /mcp(Bearer 令牌鉴权),只读:AI 只能查询出勤、工时与薪酬,改不了任何数据。开关与令牌改动即时生效,无需重启;重置后旧客户端立即失效。
|
||||
@@ -2067,6 +2067,8 @@ createApp({
|
||||
const localDays = Object.keys(records.value).length;
|
||||
const localAdj = Object.keys(monthAdjust.value).length;
|
||||
|
||||
loadMcpStatus().catch(() => {});
|
||||
|
||||
// 本机干净 → 没有可丢的东西,直接拉全量
|
||||
if (localDays === 0 && localAdj === 0) {
|
||||
await window.WHTSync.pullAll();
|
||||
@@ -2160,6 +2162,7 @@ createApp({
|
||||
const doLogout = () => withBusy(async () => {
|
||||
if (sync.pending > 0 && !confirm(`还有 ${sync.pending} 项未上传,确定退出?本地数据不会删除。`)) return;
|
||||
await window.WHTSync.logout({ keepLocal: true });
|
||||
resetMcpState(true);
|
||||
showToast('已退出登录,本地数据保留');
|
||||
});
|
||||
|
||||
@@ -2189,9 +2192,19 @@ createApp({
|
||||
// 开关与令牌存服务器 DB,改动即时生效;令牌明文只在生成/复制响应里出现,页面不留存。
|
||||
const mcp = reactive({ configured: false, enabled: false, masked: '', busy: false, loaded: false, authNeeded: false });
|
||||
const mcpCopied = ref(false);
|
||||
|
||||
const resetMcpState = (authNeeded = true) => {
|
||||
mcp.configured = false;
|
||||
mcp.enabled = false;
|
||||
mcp.masked = '';
|
||||
mcp.authNeeded = !!authNeeded;
|
||||
mcp.loaded = true;
|
||||
mcp.busy = false;
|
||||
};
|
||||
|
||||
const mcpBadge = computed(() => {
|
||||
if (!mcp.loaded) return { text: '…', cls: 'bg-slate-100 text-slate-400' };
|
||||
if (mcp.authNeeded) return { text: '本设备请先登录', cls: 'bg-amber-50 text-amber-600' };
|
||||
if (mcp.authNeeded || !sync.user) return { text: '本设备请先登录', cls: 'bg-amber-50 text-amber-600' };
|
||||
if (!mcp.configured) return { text: '未配置', cls: 'bg-slate-100 text-slate-400' };
|
||||
return mcp.enabled
|
||||
? { text: '已开启', cls: 'bg-emerald-50 text-emerald-600' }
|
||||
@@ -2207,15 +2220,24 @@ createApp({
|
||||
};
|
||||
|
||||
const loadMcpStatus = async () => {
|
||||
if (!sync.user || !window.WHTSync || !window.WHTSync.state.token) {
|
||||
resetMcpState(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(window.WHTSync.getApiBase() + '/api/mcp/status', { headers: mcpHeaders() });
|
||||
const d = await res.json();
|
||||
mcp.authNeeded = res.status === 401 || res.status === 403;
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
resetMcpState(true);
|
||||
return;
|
||||
}
|
||||
if (d && d.ok) {
|
||||
mcp.configured = !!d.configured;
|
||||
mcp.enabled = !!d.enabled;
|
||||
mcp.masked = d.masked || '';
|
||||
if (d.configured) mcp.authNeeded = false;
|
||||
mcp.authNeeded = false;
|
||||
} else {
|
||||
resetMcpState(false);
|
||||
}
|
||||
} catch (e) { /* 静态模式/离线:保持未知态,不干扰记账主功能 */ }
|
||||
mcp.loaded = true;
|
||||
@@ -2223,6 +2245,10 @@ createApp({
|
||||
|
||||
const toggleMcp = async () => {
|
||||
if (mcp.busy) return;
|
||||
if (!sync.user || mcp.authNeeded) {
|
||||
showToast('请先登录账号后再设置 MCP');
|
||||
return;
|
||||
}
|
||||
mcp.busy = true;
|
||||
try {
|
||||
const res = await fetch(window.WHTSync.getApiBase() + '/api/mcp/enabled', {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "work-hours-tracker",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.3",
|
||||
"private": true,
|
||||
"description": "工时记账:单文件前端 + 薪酬纯模块(shared/payroll.js)+ 平台无关服务端(server/)",
|
||||
"scripts": {
|
||||
|
||||
+16
-10
@@ -287,26 +287,32 @@
|
||||
}
|
||||
|
||||
// ===== 格式化 =====
|
||||
function fmt(n) { return (Number(n) || 0).toFixed(1); }
|
||||
function money(n) { return '¥' + (Number(n) || 0).toFixed(1); }
|
||||
function signed(n) {
|
||||
// 消除浮点二进制抖动,同时保留原始数据(不进行有损进位截断)
|
||||
function rawAmount(n) {
|
||||
var v = Number(n) || 0;
|
||||
return (v >= 0 ? '+¥' : '-¥') + Math.abs(v).toFixed(1);
|
||||
return Math.round((v + Number.EPSILON) * 100) / 100;
|
||||
}
|
||||
function fmt(n) { return (Number(n) || 0).toFixed(1); }
|
||||
function money(n) { return '¥' + rawAmount(n); }
|
||||
function signed(n) {
|
||||
var v = rawAmount(n);
|
||||
return (v >= 0 ? '+¥' : '-¥') + Math.abs(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额拆成 符号/整数部/小数部,供看板主数字分层渲染(大字只放整数部)。
|
||||
* 必须从 toFixed(1) 的结果上切,不能用 Math.floor 另算,
|
||||
* 否则 99.96 会被 money() 进为 100.0、而拆分出 99.0,两处数字对不上。
|
||||
* 保留原始数据的小数部分,不进行有损四舍五入。
|
||||
*/
|
||||
function moneyParts(n) {
|
||||
var v = Number(n) || 0;
|
||||
var s = Math.abs(v).toFixed(1);
|
||||
var v = rawAmount(n);
|
||||
var s = String(Math.abs(v));
|
||||
var dot = s.indexOf('.');
|
||||
var intPart = dot !== -1 ? s.slice(0, dot) : s;
|
||||
var fracPart = dot !== -1 ? s.slice(dot) : '';
|
||||
return {
|
||||
sign: v < 0 ? '-' : '',
|
||||
int: s.slice(0, dot).replace(/\B(?=(\d{3})+(?!\d))/g, ','), // 千分位
|
||||
frac: s.slice(dot)
|
||||
int: intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','), // 千分位
|
||||
frac: fracPart
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 4,
|
||||
"boardInt": "702",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -226,7 +226,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 2,
|
||||
"boardInt": "468",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -342,7 +342,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 1,
|
||||
"boardInt": "195",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -458,7 +458,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 27,
|
||||
"boardInt": "4,428",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -574,7 +574,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 26,
|
||||
"boardInt": "4,056",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -704,7 +704,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 3,
|
||||
"boardInt": "468",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -834,7 +834,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 3,
|
||||
"boardInt": "408",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -950,7 +950,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 1,
|
||||
"boardInt": "156",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -1066,7 +1066,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 1,
|
||||
"boardInt": "176",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -1182,7 +1182,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 6,
|
||||
"boardInt": "312",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -1298,7 +1298,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 6,
|
||||
"boardInt": "312",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -1414,7 +1414,7 @@
|
||||
"isCurrentMonth": false,
|
||||
"recordCount": 3,
|
||||
"boardInt": "156",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 31,
|
||||
@@ -1530,7 +1530,7 @@
|
||||
"isCurrentMonth": false,
|
||||
"recordCount": 3,
|
||||
"boardInt": "156",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 31,
|
||||
@@ -1654,7 +1654,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 1,
|
||||
"boardInt": "117",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -1778,7 +1778,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 1,
|
||||
"boardInt": "283",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "-",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -1894,7 +1894,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 4,
|
||||
"boardInt": "156",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -2010,7 +2010,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 1,
|
||||
"boardInt": "156",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -2126,7 +2126,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 3,
|
||||
"boardInt": "156",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -2242,7 +2242,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 20,
|
||||
"boardInt": "6,400",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -2358,7 +2358,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 1,
|
||||
"boardInt": "156",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -2494,7 +2494,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 4,
|
||||
"boardInt": "624",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -2610,7 +2610,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 0,
|
||||
"boardInt": "0",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -2726,7 +2726,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 3,
|
||||
"boardInt": "516",
|
||||
"boardFrac": ".8",
|
||||
"boardFrac": ".75",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -2842,7 +2842,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 3,
|
||||
"boardInt": "429",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -2958,7 +2958,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 3,
|
||||
"boardInt": "439",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -3074,7 +3074,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 27,
|
||||
"boardInt": "4,428",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -3190,7 +3190,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 27,
|
||||
"boardInt": "4,478",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
@@ -3306,7 +3306,7 @@
|
||||
"isCurrentMonth": false,
|
||||
"recordCount": 2,
|
||||
"boardInt": "351",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 31,
|
||||
@@ -3422,7 +3422,7 @@
|
||||
"isCurrentMonth": false,
|
||||
"recordCount": 2,
|
||||
"boardInt": "351",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 31,
|
||||
@@ -3538,7 +3538,7 @@
|
||||
"isCurrentMonth": true,
|
||||
"recordCount": 0,
|
||||
"boardInt": "0",
|
||||
"boardFrac": ".0",
|
||||
"boardFrac": "",
|
||||
"boardSign": "",
|
||||
"hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12",
|
||||
"daysInMonth": 30,
|
||||
|
||||
+13
-11
@@ -425,20 +425,22 @@ test('isCurrentMonth / isSameDay 只认传进来的 now', () => {
|
||||
|
||||
// ───────────────────────── 格式化 ─────────────────────────
|
||||
|
||||
test('金额格式化保留 1 位,null/undefined 当 0', () => {
|
||||
test('金额格式化保留原始数据(分位不截断,不进位),null/undefined 当 0', () => {
|
||||
assert.equal(P.fmt(undefined), '0.0');
|
||||
assert.equal(P.money(null), '¥0.0');
|
||||
assert.equal(P.money(1234.56), '¥1234.6');
|
||||
assert.equal(P.signed(0), '+¥0.0');
|
||||
assert.equal(P.signed(-5), '-¥5.0');
|
||||
assert.equal(P.signed(30), '+¥30.0');
|
||||
assert.equal(P.money(null), '¥0');
|
||||
assert.equal(P.money(1234.56), '¥1234.56');
|
||||
assert.equal(P.money(2018.25), '¥2018.25');
|
||||
assert.equal(P.signed(0), '+¥0');
|
||||
assert.equal(P.signed(-5), '-¥5');
|
||||
assert.equal(P.signed(-50.25), '-¥50.25');
|
||||
assert.equal(P.signed(30), '+¥30');
|
||||
});
|
||||
|
||||
test('moneyParts 从 toFixed(1) 的结果上切,不另算整数部', () => {
|
||||
// 99.96 若用 Math.floor 另算会得到 99,与 money() 的 ¥100.0 打架
|
||||
assert.deepEqual(P.moneyParts(99.96), { sign: '', int: '100', frac: '.0' });
|
||||
assert.deepEqual(P.moneyParts(-1234.56), { sign: '-', int: '1,234', frac: '.6' });
|
||||
assert.deepEqual(P.moneyParts(0), { sign: '', int: '0', frac: '.0' });
|
||||
test('moneyParts 保留原始数据小数位,千分位与小数部拆分对齐', () => {
|
||||
assert.deepEqual(P.moneyParts(99.96), { sign: '', int: '99', frac: '.96' });
|
||||
assert.deepEqual(P.moneyParts(2018.25), { sign: '', int: '2,018', frac: '.25' });
|
||||
assert.deepEqual(P.moneyParts(-1234.56), { sign: '-', int: '1,234', frac: '.56' });
|
||||
assert.deepEqual(P.moneyParts(0), { sign: '', int: '0', frac: '' });
|
||||
assert.deepEqual(P.moneyParts(1234567.8), { sign: '', int: '1,234,567', frac: '.8' });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user