feat: support credit transfers and precious metal valuations
This commit is contained in:
1 parent
425b45c91a
commit
518aea1e59
39 files changed
+1572
-77
No files matched your search
+51
-21
@@ -54,6 +54,7 @@ import { orderedGroups, mergeGroupOrder } from './group-order';
|
||||
import { Calendar } from './Calendar';
|
||||
import { SchedulePanel } from './SchedulePanel';
|
||||
import { IconPicker } from './IconPicker';
|
||||
import { MetalPanel } from './MetalPanel';
|
||||
import { QuickTransfer } from './QuickTransfer';
|
||||
import { TransferForm } from './TransferForm';
|
||||
import { DebtPaymentForm, operationLabel, type DebtOperation } from './DebtPaymentForm';
|
||||
@@ -259,11 +260,7 @@ export default function App() {
|
||||
setQuickTransfer((current) =>
|
||||
current &&
|
||||
[current.sourceId, current.targetId].some(
|
||||
(id) =>
|
||||
id &&
|
||||
!positions.some(
|
||||
(p) => p.id === id && p.kind === 'account' && p.side === 'asset' && !p.archived,
|
||||
),
|
||||
(id) => id && !positions.some((p) => p.id === id && p.kind === 'account' && !p.archived),
|
||||
)
|
||||
? null
|
||||
: current,
|
||||
@@ -271,8 +268,8 @@ export default function App() {
|
||||
}, [positions]);
|
||||
function chooseQuickAccount(p: Position) {
|
||||
if (busy || loading) return;
|
||||
if (p.kind !== 'account' || p.side !== 'asset' || p.archived) {
|
||||
setError(tr('请选择启用的资产账户'));
|
||||
if (p.kind !== 'account' || p.archived) {
|
||||
setError(tr('请选择启用的账户'));
|
||||
return;
|
||||
}
|
||||
setQuickTransfer((current) => {
|
||||
@@ -1152,7 +1149,8 @@ export default function App() {
|
||||
<span>
|
||||
{tr(labels[k])}
|
||||
<small>
|
||||
{overview.items.filter((p) => p.kind === k).length} {tr('个项目')}
|
||||
{overview.items.filter((p) => p.included && p.kind === k).length}{' '}
|
||||
{tr('个项目')}
|
||||
</small>
|
||||
</span>
|
||||
<ArrowUpRight size={18} />
|
||||
@@ -1274,10 +1272,14 @@ export default function App() {
|
||||
: tr('缺少汇率')}
|
||||
</p>
|
||||
<p className="notes-display">{p.notes || tr('暂无备注')}</p>
|
||||
{(p.included === false ||
|
||||
(p.kind === 'asset' && user.includeIndependentAssets === false)) && (
|
||||
<span className="badge">{tr('不计入总览')}</span>
|
||||
)}
|
||||
<small>{tr('币种和资产负债属性创建后固定。归档项目仍计入总览。')}</small>
|
||||
</div>
|
||||
<div className="actions">
|
||||
{p.kind === 'account' && p.side === 'asset' && !p.archived && (
|
||||
{p.kind === 'account' && !p.archived && (
|
||||
<>
|
||||
<button
|
||||
className="primary"
|
||||
@@ -1286,6 +1288,7 @@ export default function App() {
|
||||
{tr('转账')}
|
||||
</button>
|
||||
<button
|
||||
hidden={p.side !== 'asset'}
|
||||
className="secondary"
|
||||
onClick={() => setModal({ kind: 'debt-payment', p })}
|
||||
>
|
||||
@@ -1342,6 +1345,9 @@ export default function App() {
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
{p.kind === 'asset' && p.category === 'gold' && (
|
||||
<MetalPanel position={p} refresh={() => load(false, true)} />
|
||||
)}
|
||||
{p.kind === 'debt' && (
|
||||
<section className="panel">
|
||||
<div className="panel-title">
|
||||
@@ -1479,9 +1485,7 @@ export default function App() {
|
||||
? quickTransfer.sourceId === p.id || quickTransfer.targetId === p.id
|
||||
: undefined
|
||||
}
|
||||
disabled={
|
||||
busy || (!!quickTransfer && (p.side !== 'asset' || p.archived))
|
||||
}
|
||||
disabled={busy || (!!quickTransfer && p.archived)}
|
||||
key={p.id}
|
||||
onClick={() =>
|
||||
quickTransfer !== null && page === 'account' && quickMode
|
||||
@@ -1507,6 +1511,10 @@ export default function App() {
|
||||
</div>
|
||||
<h3>{p.name}</h3>
|
||||
{p.groupName && <span className="badge">{p.groupName}</span>}
|
||||
{(p.included === false ||
|
||||
(p.kind === 'asset' && user.includeIndependentAssets === false)) && (
|
||||
<span className="badge">{tr('不计入总览')}</span>
|
||||
)}
|
||||
<p>
|
||||
{categoryLabel(p)} ·{' '}
|
||||
{p.kind === 'debt'
|
||||
@@ -1555,9 +1563,7 @@ export default function App() {
|
||||
disabled={
|
||||
busy ||
|
||||
loading ||
|
||||
positions.filter(
|
||||
(p) => p.kind === 'account' && p.side === 'asset' && !p.archived,
|
||||
).length < 2
|
||||
positions.filter((p) => p.kind === 'account' && !p.archived).length < 2
|
||||
}
|
||||
onClick={() => setQuickTransfer({})}
|
||||
>
|
||||
@@ -1729,9 +1735,7 @@ export default function App() {
|
||||
{page === 'schedules' && (
|
||||
<section className="panel">
|
||||
<SchedulePanel
|
||||
accounts={positions.filter(
|
||||
(p) => p.kind === 'account' && p.side === 'asset' && !p.archived,
|
||||
)}
|
||||
accounts={positions.filter((p) => p.kind === 'account' && !p.archived)}
|
||||
refresh={() => load(false, true)}
|
||||
/>
|
||||
</section>
|
||||
@@ -1898,12 +1902,26 @@ export default function App() {
|
||||
'schedules',
|
||||
].filter((key) => !f.has('menu_' + key)),
|
||||
showNotes: f.has('showNotes'),
|
||||
includeIndependentAssets: f.has('includeIndependentAssets'),
|
||||
overviewCards: allOverviewCards.filter((key) => f.has('card_' + key)),
|
||||
}),
|
||||
tr('设置已保存'),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<label className="switch-row">
|
||||
<span>{tr('独立资产计入总览')}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
name="includeIndependentAssets"
|
||||
defaultChecked={user.includeIndependentAssets !== false}
|
||||
key={String(user.includeIndependentAssets)}
|
||||
/>
|
||||
</label>
|
||||
<p className="muted">
|
||||
{tr('关闭后,独立资产不参与总额和净资产轨迹,资料与历史仍保留。')}
|
||||
</p>
|
||||
<h3>{tr('导航菜单')}</h3>
|
||||
<p className="muted">
|
||||
{tr('选择需要显示的菜单项。设置入口始终保留,隐藏仅影响导航,不删除数据。')}
|
||||
@@ -2136,6 +2154,7 @@ export default function App() {
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
{settingsSection === 'rates' && <MetalPanel refresh={() => load(false, true)} />}
|
||||
<section className="panel" hidden={settingsSection !== 'clear'}>
|
||||
<h2 className="danger-text">{tr('清空本账号数据')}</h2>
|
||||
<p className="muted">
|
||||
@@ -2546,9 +2565,7 @@ export default function App() {
|
||||
<p>{tr('正在刷新数据…')}</p>
|
||||
) : modal.kind === 'transfer' ? (
|
||||
<TransferForm
|
||||
accounts={movementPositions.filter(
|
||||
(p) => p.kind === 'account' && p.side === 'asset' && !p.archived,
|
||||
)}
|
||||
accounts={movementPositions.filter((p) => p.kind === 'account' && !p.archived)}
|
||||
initialSourceId={modal.p?.kind === 'account' ? modal.p.id : undefined}
|
||||
busy={busy}
|
||||
submit={(v) =>
|
||||
@@ -2621,6 +2638,7 @@ export default function App() {
|
||||
...(f.has('iconId') ? { iconId: f.get('iconId') || null } : {}),
|
||||
archived: mp!.archived,
|
||||
hidden: f.get('hidden') === 'on',
|
||||
included: f.get('included') === 'on',
|
||||
}),
|
||||
tr('项目资料已保存'),
|
||||
);
|
||||
@@ -2641,6 +2659,7 @@ export default function App() {
|
||||
side,
|
||||
amount: accountInputAmount({ kind, side }, String(v.amount)),
|
||||
hidden: f.get('hidden') === 'on',
|
||||
included: f.get('included') === 'on',
|
||||
}),
|
||||
tr('项目已添加'),
|
||||
);
|
||||
@@ -2660,6 +2679,17 @@ export default function App() {
|
||||
)}
|
||||
{['account', 'asset', 'debt', 'edit'].includes(modal.kind) && (
|
||||
<>
|
||||
<label className="check-line">
|
||||
<input
|
||||
name="included"
|
||||
type="checkbox"
|
||||
defaultChecked={modal.p?.included !== false}
|
||||
/>
|
||||
{tr('计入资产总览')}
|
||||
</label>
|
||||
<p className="muted">
|
||||
{tr('取消后不参与资产、负债及净资产轨迹,仍可管理和转账。')}
|
||||
</p>
|
||||
<label className="check-line">
|
||||
<input name="hidden" type="checkbox" defaultChecked={modal.p?.hidden} />
|
||||
{tr('隐藏此项目(密码验证后可查看和编辑)')}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api, money, type Position, currencies } from './api';
|
||||
import { t as tr } from './i18n';
|
||||
import { useToast } from './Toast';
|
||||
type Quote = { metalType: string; currency: string; price: string; date: string; source: string };
|
||||
type Data = { status: { state: string; message: string }; prices: Quote[] };
|
||||
export function MetalPanel({
|
||||
position,
|
||||
refresh,
|
||||
}: {
|
||||
position?: Position;
|
||||
refresh: () => Promise<void>;
|
||||
}) {
|
||||
const [data, setData] = useState<Data>(),
|
||||
[busy, setBusy] = useState(false),
|
||||
[error, setError] = useState(''),
|
||||
[message, setMessage] = useState('');
|
||||
useToast(error, 'error');
|
||||
useToast(message, 'success');
|
||||
async function load() {
|
||||
setData(await api<Data>('/metals'));
|
||||
}
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
void api<Data>('/metals')
|
||||
.then((v) => {
|
||||
if (active) setData(v);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (active) setError(e.message);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [position]);
|
||||
useEffect(() => {
|
||||
if (data?.status.state !== 'updating') return;
|
||||
let active = true;
|
||||
const timer = setInterval(() => {
|
||||
void api<Data>('/metals')
|
||||
.then((v) => {
|
||||
if (active) setData(v);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (active) {
|
||||
setError(e.message);
|
||||
clearInterval(timer);
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
return () => {
|
||||
active = false;
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [data?.status.state]);
|
||||
async function act(work: () => Promise<unknown>, message: string) {
|
||||
setBusy(true);
|
||||
setError('');
|
||||
setMessage('');
|
||||
try {
|
||||
await work();
|
||||
await load();
|
||||
await refresh();
|
||||
setMessage(tr(message));
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
await load().catch(() => {});
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
const quote = data?.prices.find(
|
||||
(q) => q.metalType === position?.metalType && q.currency === position?.currency,
|
||||
);
|
||||
return (
|
||||
<section className="panel">
|
||||
<h2>{tr('贵金属估价')}</h2>
|
||||
<p className="muted">
|
||||
{tr(
|
||||
'黄金与白银按重量、纯度及每克参考价估值。每日尝试更新,失败保留旧价;参考价不含工费与回收折价。',
|
||||
)}
|
||||
</p>
|
||||
{position && (
|
||||
<form
|
||||
key={
|
||||
position.id +
|
||||
String(position.metalGrams) +
|
||||
String(position.autoValuation) +
|
||||
String(position.metalPurity) +
|
||||
String(position.metalType)
|
||||
}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const f = new FormData(e.currentTarget);
|
||||
void act(
|
||||
() =>
|
||||
api('/metals/' + position.id, 'PUT', {
|
||||
metalType: f.get('metalType'),
|
||||
metalGrams: f.get('metalGrams'),
|
||||
metalPurity: f.get('metalPurity'),
|
||||
autoValuation: f.has('autoValuation'),
|
||||
}),
|
||||
'贵金属估价设置已保存',
|
||||
);
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
{tr('贵金属品种')}
|
||||
<select name="metalType" defaultValue={position.metalType || 'gold'}>
|
||||
<option value="gold">{tr('黄金')}</option>
|
||||
<option value="silver">{tr('白银')}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{tr('重量(克)')}
|
||||
<input
|
||||
name="metalGrams"
|
||||
required
|
||||
inputMode="decimal"
|
||||
pattern="(0|[1-9][0-9]{0,15})([.][0-9]{1,8})?"
|
||||
defaultValue={position.metalGrams || ''}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{tr('纯度(0–1,例如 0.999)')}
|
||||
<input
|
||||
name="metalPurity"
|
||||
required
|
||||
type="number"
|
||||
min="0.00000001"
|
||||
max="1"
|
||||
step="0.00000001"
|
||||
defaultValue={position.metalPurity || '1'}
|
||||
/>
|
||||
</label>
|
||||
<label className="check-line">
|
||||
<input type="checkbox" name="autoValuation" defaultChecked={position.autoValuation} />
|
||||
{tr('随参考价自动记录估值')}
|
||||
</label>
|
||||
<button className="primary" disabled={busy || position.archived}>
|
||||
{tr('保存估价设置')}
|
||||
</button>
|
||||
<p className="muted">
|
||||
{tr('开启后,每日价格更新会新增估值历史;已有历史不会改写。关闭后可手动应用估价。')}
|
||||
</p>
|
||||
</form>
|
||||
)}
|
||||
<div className="actions">
|
||||
<button
|
||||
className="secondary"
|
||||
disabled={busy}
|
||||
onClick={() => void act(() => api('/metals/refresh', 'POST', {}), '贵金属参考价已更新')}
|
||||
>
|
||||
{tr('刷新贵金属价格')}
|
||||
</button>
|
||||
{position && (
|
||||
<button
|
||||
className="primary"
|
||||
disabled={busy || !quote || position.archived}
|
||||
onClick={() =>
|
||||
void act(() => api('/metals/' + position.id + '/value', 'POST', {}), '估价已记入历史')
|
||||
}
|
||||
>
|
||||
{tr('应用估价')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{quote && (
|
||||
<p>
|
||||
{tr('参考价')}:{money(quote.price, quote.currency)}/{tr('克')} ·{' '}
|
||||
{quote.date.slice(0, 10)} · {quote.source === 'manual' ? tr('手动') : 'Gold API'}
|
||||
</p>
|
||||
)}
|
||||
<p className={data?.status.state === 'error' ? 'danger-text' : 'muted'}>
|
||||
{tr(data?.status.message || '尚未尝试更新')}
|
||||
</p>
|
||||
<details>
|
||||
<summary>{tr('手动录入贵金属价格')}</summary>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const f = new FormData(e.currentTarget);
|
||||
void act(
|
||||
() => api('/metals/prices', 'POST', Object.fromEntries(f)),
|
||||
'贵金属价格已保存',
|
||||
);
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
{tr('贵金属品种')}
|
||||
<select name="metalType" defaultValue={position?.metalType || 'gold'}>
|
||||
<option value="gold">{tr('黄金')}</option>
|
||||
<option value="silver">{tr('白银')}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{tr('币种')}
|
||||
<select name="currency" defaultValue={position?.currency || 'CNY'}>
|
||||
{currencies.map((c) => (
|
||||
<option key={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{tr('每克纯金属价格')}
|
||||
<input
|
||||
name="price"
|
||||
required
|
||||
inputMode="decimal"
|
||||
pattern="(0|[1-9][0-9]{0,11})([.][0-9]{1,12})?"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{tr('报价日期')}
|
||||
<input
|
||||
name="date"
|
||||
type="date"
|
||||
required
|
||||
max={new Date(Date.now() + 8 * 3600000).toISOString().slice(0, 10)}
|
||||
defaultValue={new Date(Date.now() + 8 * 3600000).toISOString().slice(0, 10)}
|
||||
/>
|
||||
</label>
|
||||
<button className="primary" disabled={busy}>
|
||||
{tr('保存参考价')}
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
{!position && (
|
||||
<div className="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{tr('贵金属品种')}</th>
|
||||
<th>{tr('参考价')}</th>
|
||||
<th>{tr('报价日期')}</th>
|
||||
<th>{tr('来源')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.prices.map((q) => (
|
||||
<tr key={q.metalType + q.currency + q.date}>
|
||||
<td>{tr(q.metalType === 'gold' ? '黄金' : '白银')}</td>
|
||||
<td>
|
||||
{money(q.price, q.currency)}/{tr('克')}
|
||||
</td>
|
||||
<td>{q.date.slice(0, 10)}</td>
|
||||
<td>{q.source === 'manual' ? tr('手动') : 'Gold API'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { money, nowMinute, requestUuid, type Position } from './api';
|
||||
import { money, positionAmount, nowMinute, requestUuid, type Position } from './api';
|
||||
import { t as tr } from './i18n';
|
||||
export function QuickTransfer({
|
||||
source,
|
||||
@@ -40,7 +40,7 @@ export function QuickTransfer({
|
||||
<div>
|
||||
<small>{tr('转出账户')}</small>
|
||||
<strong>{source.name}</strong>
|
||||
<span>{money(source.amount, source.currency)}</span>
|
||||
<span>{money(positionAmount(source), source.currency)}</span>
|
||||
<button type="button" className="text" disabled={busy} onClick={changeSource}>
|
||||
{tr('重选')}
|
||||
</button>
|
||||
@@ -49,7 +49,7 @@ export function QuickTransfer({
|
||||
<div>
|
||||
<small>{tr('转入账户')}</small>
|
||||
<strong>{target.name}</strong>
|
||||
<span>{money(target.amount, target.currency)}</span>
|
||||
<span>{money(positionAmount(target), target.currency)}</span>
|
||||
<button type="button" className="text" disabled={busy} onClick={changeTarget}>
|
||||
{tr('重选')}
|
||||
</button>
|
||||
|
||||
@@ -47,7 +47,11 @@ export function SchedulePanel({
|
||||
[source, setSource] = useState(''),
|
||||
[target, setTarget] = useState(''),
|
||||
[amount, setAmount] = useState('');
|
||||
const sourceId = accounts.some((p) => p.id === source) ? source : accounts[0]?.id || '';
|
||||
const sourceAccounts =
|
||||
operation === 'expense' ? accounts.filter((p) => p.side === 'asset') : accounts;
|
||||
const sourceId = sourceAccounts.some((p) => p.id === source)
|
||||
? source
|
||||
: sourceAccounts[0]?.id || '';
|
||||
const sourceAccount = accounts.find((p) => p.id === sourceId),
|
||||
targetAccount = accounts.find((p) => p.id === target);
|
||||
const same = sourceAccount?.currency === targetAccount?.currency;
|
||||
@@ -173,7 +177,7 @@ export function SchedulePanel({
|
||||
{tr('付款账户')}
|
||||
<AccountPicker
|
||||
name="sourceId"
|
||||
accounts={accounts}
|
||||
accounts={sourceAccounts}
|
||||
value={sourceId}
|
||||
onChange={(id) => {
|
||||
setSource(id);
|
||||
|
||||
@@ -27,7 +27,7 @@ export function TransferForm({
|
||||
submit(Object.fromEntries(new FormData(e.currentTarget)));
|
||||
}
|
||||
if (accounts.length < 2)
|
||||
return <p>{tr('请先创建至少两个启用中的资产账户。隐藏账户需验证密码后才能选择。')}</p>;
|
||||
return <p>{tr('请先创建至少两个启用中的账户。隐藏账户需验证密码后才能选择。')}</p>;
|
||||
return (
|
||||
<form onSubmit={save} className="transfer-form">
|
||||
<input type="hidden" name="requestId" value={requestId} />
|
||||
|
||||
@@ -27,6 +27,7 @@ export type User = {
|
||||
sessionHours?: number;
|
||||
requireHiddenPassword?: boolean;
|
||||
overviewCards?: string[];
|
||||
includeIndependentAssets?: boolean;
|
||||
sessionExpiresAt?: string;
|
||||
revealed?: boolean;
|
||||
revealUntil?: string | null;
|
||||
@@ -50,6 +51,11 @@ export type History = {
|
||||
reason: string;
|
||||
};
|
||||
export type Position = {
|
||||
included?: boolean;
|
||||
metalType?: string | null;
|
||||
metalGrams?: string | null;
|
||||
metalPurity?: string;
|
||||
autoValuation?: boolean;
|
||||
groupName?: string;
|
||||
iconId?: string | null;
|
||||
id: string;
|
||||
@@ -83,6 +89,7 @@ export type Overview = Total & {
|
||||
recent: History[];
|
||||
items: {
|
||||
id: string;
|
||||
included: boolean;
|
||||
kind: string;
|
||||
converted: string | null;
|
||||
rateDate: string | null;
|
||||
|
||||
@@ -519,5 +519,37 @@
|
||||
"关闭后,已登录账号可直接显示隐藏资产。每次显示持续 5 分钟,也可手动锁定;保存此设置会锁定所有会话的隐藏资产。": "When disabled, a signed-in account can reveal hidden assets directly for 5 minutes. You can lock them sooner. Saving this setting locks hidden assets in all sessions.",
|
||||
"隐藏资产已显示": "hidden assets revealed",
|
||||
"显示隐藏资产": "Reveal hidden assets",
|
||||
"当前包含隐藏资产 · 5 分钟后自动锁定": "hidden assets included · Locks automatically in 5 minutes"
|
||||
"当前包含隐藏资产 · 5 分钟后自动锁定": "hidden assets included · Locks automatically in 5 minutes",
|
||||
"独立资产计入总览": "Include standalone assets in overview",
|
||||
"关闭后,独立资产不参与总额和净资产轨迹,资料与历史仍保留。": "Exclude standalone assets from totals and net worth history while keeping their records.",
|
||||
"计入资产总览": "Include in portfolio totals",
|
||||
"取消后不参与资产、负债及净资产轨迹,仍可管理和转账。": "Exclude from assets, liabilities and net worth history; management and transfers remain available.",
|
||||
"不计入总览": "Excluded from totals",
|
||||
"贵金属估价": "Precious metal valuation",
|
||||
"黄金与白银按重量、纯度及每克参考价估值。每日尝试更新,失败保留旧价;参考价不含工费与回收折价。": "Value gold and silver by weight, purity and reference price per gram. Daily updates keep previous prices on failure; workmanship and resale discounts are excluded.",
|
||||
"贵金属品种": "Metal",
|
||||
"黄金": "Gold",
|
||||
"白银": "Silver",
|
||||
"重量(克)": "Weight (grams)",
|
||||
"纯度(0–1,例如 0.999)": "Purity (0–1, e.g. 0.999)",
|
||||
"随参考价自动记录估值": "Record valuations automatically with price updates",
|
||||
"保存估价设置": "Save valuation settings",
|
||||
"开启后,每日价格更新会新增估值历史;已有历史不会改写。关闭后可手动应用估价。": "Automatic updates add valuation history and preserve earlier records. You can also apply valuations manually.",
|
||||
"刷新贵金属价格": "Refresh metal prices",
|
||||
"应用估价": "Apply valuation",
|
||||
"参考价": "Reference price",
|
||||
"克": "g",
|
||||
"手动录入贵金属价格": "Enter a metal price manually",
|
||||
"每克纯金属价格": "Pure metal price per gram",
|
||||
"报价日期": "Quote date",
|
||||
"保存参考价": "Save reference price",
|
||||
"贵金属估价设置已保存": "Valuation settings saved",
|
||||
"贵金属参考价已更新": "Metal prices updated",
|
||||
"估价已记入历史": "Valuation recorded",
|
||||
"贵金属价格已保存": "Metal price saved",
|
||||
"当前没有已设置重量的贵金属资产": "No configured precious metal holdings",
|
||||
"正在更新贵金属参考价": "Updating reference prices",
|
||||
"贵金属价格更新失败,已有价格与估值已保留,可手动录价": "Price update failed. Previous prices and values are preserved; you can enter prices manually.",
|
||||
"请先创建至少两个启用中的账户。隐藏账户需验证密码后才能选择。": "Create at least two active accounts. Unlock hidden accounts before selecting them.",
|
||||
"请选择启用的账户": "Choose an active account"
|
||||
}
|
||||
@@ -519,5 +519,37 @@
|
||||
"关闭后,已登录账号可直接显示隐藏资产。每次显示持续 5 分钟,也可手动锁定;保存此设置会锁定所有会话的隐藏资产。": "關閉後,已登入帳號可直接顯示隱藏資產。每次顯示持續 5 分鐘,也可手動鎖定;儲存此設定會鎖定所有會話的隱藏資產。",
|
||||
"隐藏资产已显示": "隱藏資產已顯示",
|
||||
"显示隐藏资产": "顯示隱藏資產",
|
||||
"当前包含隐藏资产 · 5 分钟后自动锁定": "目前包含隱藏資產 · 5 分鐘後自動鎖定"
|
||||
"当前包含隐藏资产 · 5 分钟后自动锁定": "目前包含隱藏資產 · 5 分鐘後自動鎖定",
|
||||
"独立资产计入总览": "獨立資產計入總覽",
|
||||
"关闭后,独立资产不参与总额和净资产轨迹,资料与历史仍保留。": "關閉後,獨立資產不參與總額和淨資產軌跡,資料與歷史仍保留。",
|
||||
"计入资产总览": "計入資產總覽",
|
||||
"取消后不参与资产、负债及净资产轨迹,仍可管理和转账。": "取消後不參與資產、負債及淨資產軌跡,仍可管理和轉賬。",
|
||||
"不计入总览": "不計入總覽",
|
||||
"贵金属估价": "貴金屬估價",
|
||||
"黄金与白银按重量、纯度及每克参考价估值。每日尝试更新,失败保留旧价;参考价不含工费与回收折价。": "黃金與白銀按重量、純度及每克參考價估值。每日嘗試更新,失敗保留舊價;參考價不含工費與回收折價。",
|
||||
"贵金属品种": "貴金屬品種",
|
||||
"黄金": "黃金",
|
||||
"白银": "白銀",
|
||||
"重量(克)": "重量(克)",
|
||||
"纯度(0–1,例如 0.999)": "純度(0–1,例如 0.999)",
|
||||
"随参考价自动记录估值": "隨參考價自動記錄估值",
|
||||
"保存估价设置": "儲存估價設定",
|
||||
"开启后,每日价格更新会新增估值历史;已有历史不会改写。关闭后可手动应用估价。": "開啟後,每日價格更新會新增估值歷史;已有歷史不會改寫。關閉後可手動應用估價。",
|
||||
"刷新贵金属价格": "更新貴金屬價格",
|
||||
"应用估价": "應用估價",
|
||||
"参考价": "參考價",
|
||||
"克": "克",
|
||||
"手动录入贵金属价格": "手動輸入貴金屬價格",
|
||||
"每克纯金属价格": "每克純金屬價格",
|
||||
"报价日期": "報價日期",
|
||||
"保存参考价": "儲存參考價",
|
||||
"贵金属估价设置已保存": "貴金屬估價設定已儲存",
|
||||
"贵金属参考价已更新": "貴金屬參考價已更新",
|
||||
"估价已记入历史": "估價已記入歷史",
|
||||
"贵金属价格已保存": "貴金屬價格已儲存",
|
||||
"当前没有已设置重量的贵金属资产": "目前沒有已設定重量的貴金屬資產",
|
||||
"正在更新贵金属参考价": "正在更新貴金屬參考價",
|
||||
"贵金属价格更新失败,已有价格与估值已保留,可手动录价": "貴金屬價格更新失敗,已有價格與估值已保留,可手動輸入價格",
|
||||
"请先创建至少两个启用中的账户。隐藏账户需验证密码后才能选择。": "請先建立至少兩個啟用中的賬戶。隱藏賬戶需驗證密碼後才能選擇。",
|
||||
"请选择启用的账户": "請選擇啟用的賬戶"
|
||||
}
|
||||
Reference in new issue
Block a user