Files
happy-export/frontend/src/currencies.js
T
Drsakura 13d67c804f feat: 多用户权限体系、AI 助手、SKU 目录与合同清洗(开源准备)
本地长期开发成果的整体入库快照,包含后端鉴权/权限、AI 接入、合同解析、
SKU 目录等模块,以及前端相应页面与样式改版。

后端
- 新增鉴权与会话:scrypt 密码哈希、HttpOnly cookie(happy_sid)、登录失败锁定
- 新增「角色 → 权限组 → 权限点」三级权限模型与审计日志
- 新增 AI 提供商接入与预设(兼容 OpenAI 协议,内置 DeepSeek / 通义千问等)
- 新增合同解析、合同文件保留策略、客户归属与访问控制
- 新增 SKU 目录、组织品牌、新客开发、系统日志等路由

前端
- 新增登录页与用户 / 权限组 / 系统日志管理页
- 新增客户详情、客户公海、询盘详情、报价编辑器、合同清洗中心
- 新增 AI 配置、组织品牌、包装参数、SKU 选择器等组件
- 新增英文大写金额、未保存改动拦截、抽屉与骨架屏
- 新增 stage1 样式层;产品、供应商、订单等页面改版
- EmbeddedSku 页面并入现有产品体系后删除

开源准备
- 新增 AGPL-3.0 LICENSE
- README:移除未启用的 SQLCipher 加密声明(改为如实的「已知限制」)、
  补充环境变量说明、许可证由「私有项目」改为 AGPL-3.0
- 移除历史遗留的空文件 _index_backup.txt
- 文档中的项目路径更新为 happy-export
2026-09-17 14:53:02 +08:00

144 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 币种清单(顶栏汇率计算器用)
*
* 只是「展示用」的名字与排序。真正有哪些币种、汇率多少,全部以
* 后端 /api/fx/rates 返回的 rates 为准 —— 接口给了 160+ 币种,
* 这份清单里没列到的也会照常出现在下拉里(只显示代码)。
*
* ORDER 决定下拉里的先后(外贸常用币种在前),未列出的按字母序排在后面。
*/
export const CURRENCY_NAMES = {
USD: '美元', CNY: '人民币', EUR: '欧元', GBP: '英镑', HKD: '港币', JPY: '日元',
AUD: '澳元', CAD: '加元', SGD: '新加坡元', CHF: '瑞士法郎', NZD: '新西兰元', KRW: '韩元',
AED: '阿联酋迪拉姆', SAR: '沙特里亚尔', RUB: '卢布', INR: '印度卢比', BRL: '巴西雷亚尔',
MXN: '墨西哥比索', THB: '泰铢', VND: '越南盾', MYR: '马来西亚林吉特', IDR: '印尼盾',
PHP: '菲律宾比索', TRY: '土耳其里拉', ZAR: '南非兰特', PLN: '波兰兹罗提',
SEK: '瑞典克朗', NOK: '挪威克朗', DKK: '丹麦克朗', EGP: '埃及镑', NGN: '尼日利亚奈拉',
PKR: '巴基斯坦卢比', BDT: '孟加拉塔卡', KES: '肯尼亚先令', KWD: '科威特第纳尔',
QAR: '卡塔尔里亚尔', OMR: '阿曼里亚尔', BHD: '巴林第纳尔', ILS: '以色列谢克尔',
TWD: '新台币', CLP: '智利比索', COP: '哥伦比亚比索', PEN: '秘鲁索尔', ARS: '阿根廷比索',
UAH: '乌克兰格里夫纳', CZK: '捷克克朗', HUF: '匈牙利福林', RON: '罗马尼亚列伊',
LKR: '斯里兰卡卢比', NPR: '尼泊尔卢比', MMK: '缅甸元', KHR: '柬埔寨瑞尔',
TZS: '坦桑尼亚先令', MAD: '摩洛哥迪拉姆', GHS: '加纳塞地', ETB: '埃塞俄比亚比尔',
UGX: '乌干达先令', ZMW: '赞比亚克瓦查', DZD: '阿尔及利亚第纳尔', TND: '突尼斯第纳尔',
JOD: '约旦第纳尔', IQD: '伊拉克第纳尔', LBP: '黎巴嫩镑', AZN: '阿塞拜疆马纳特',
KZT: '哈萨克斯坦坚戈', UZS: '乌兹别克斯坦苏姆', GEL: '格鲁吉亚拉里', AMD: '亚美尼亚德拉姆',
MNT: '蒙古图格里克', KGS: '吉尔吉斯斯坦索姆', BND: '文莱元', MOP: '澳门元',
ISK: '冰岛克朗', UYU: '乌拉圭比索', BOB: '玻利维亚诺', PYG: '巴拉圭瓜拉尼',
CRC: '哥斯达黎加科朗', GTQ: '危地马拉格查尔', DOP: '多米尼加比索', JMD: '牙买加元',
TTD: '特立尼达和多巴哥元', RSD: '塞尔维亚第纳尔', BGN: '保加利亚列弗',
HRK: '克罗地亚库纳', ALL: '阿尔巴尼亚列克', MKD: '北马其顿第纳尔'
}
/**
* 汇率计算器用的「主流币种」白名单,按 2024 年中国对其出口金额从高到低排。
*
* 排序依据:联合国 COMTRADE / Trading Economics 2024 年中国出口目的地金额。
* 美国 5257 亿 > 中国香港 2910 亿 > 日本 1520 亿 > 俄罗斯 1153 亿 >
* 马来西亚 1015 亿 > 新加坡 791 亿 > 中国台湾 753 亿 > 澳大利亚 708 亿 >
* 阿联酋 656 亿 > 沙特 500 亿 > 加拿大 465 亿。
* 欧元按欧元区合计(德 1071 亿 + 荷 912 亿 + 意 462 亿 + 法 450 亿 + 西 409 亿 …)
* 计入,量级仅次于美国,所以排在美国之后。人民币是本币,按需求紧随美元。
*/
export const PRIMARY_CURRENCIES = [
'USD', 'CNY', 'EUR', 'HKD', 'JPY', 'RUB', 'MYR', 'SGD', 'TWD', 'AUD', 'AED', 'SAR', 'CAD'
]
/** 币种对应的国家 / 地区(中国香港、中国台湾按中国区划标注)。 */
export const CURRENCY_REGIONS = {
USD: '美国', CNY: '中国', EUR: '欧元区', HKD: '中国香港', JPY: '日本',
RUB: '俄罗斯', MYR: '马来西亚', SGD: '新加坡', TWD: '中国台湾', AUD: '澳大利亚',
AED: '阿联酋', SAR: '沙特阿拉伯', CAD: '加拿大'
}
/**
* 汇率计算器的币种下拉:只给主流 13 种,顺序完全由 PRIMARY_CURRENCIES 决定。
* 不用看接口返回的 160+ 币种 —— 这是「常用币种快捷入口」,不是全量币种表。
*/
export function buildPrimaryCurrencyOptions() {
return PRIMARY_CURRENCIES.map(code => ({
code,
name: CURRENCY_NAMES[code] || '',
region: CURRENCY_REGIONS[code] || ''
}))
}
/** 下拉排序权重:外贸结算最常见的排前面。 */
const ORDER = [
'USD', 'CNY', 'EUR', 'GBP', 'HKD', 'JPY', 'AUD', 'CAD', 'SGD', 'CHF', 'NZD', 'KRW',
'AED', 'SAR', 'RUB', 'INR', 'BRL', 'MXN', 'THB', 'VND', 'MYR', 'IDR', 'PHP', 'TRY',
'ZAR', 'PLN', 'SEK', 'NOK', 'DKK', 'TWD', 'EGP', 'NGN', 'PKR', 'BDT', 'KES', 'KWD',
'QAR', 'OMR', 'BHD', 'ILS', 'CLP', 'COP', 'PEN', 'ARS', 'UAH', 'CZK', 'HUF', 'RON'
]
const RANK = new Map(ORDER.map((code, index) => [code, index]))
/**
* 把接口返回的 rates 变成下拉选项,按常用度排序。
* @param {Record<string, number>} rates 以 USD 为基准的汇率表
* @param {string[]} extra 兜底币种(接口没回来时也要有 USD/CNY 可选)
*/
export function buildCurrencyOptions(rates, extra = ['USD', 'CNY']) {
const codes = Object.keys(rates || {})
const merged = Array.from(new Set([...extra, ...codes]))
return merged
.map(code => ({ code, name: CURRENCY_NAMES[code] || '' }))
.sort((a, b) => {
const ra = RANK.has(a.code) ? RANK.get(a.code) : 999
const rb = RANK.has(b.code) ? RANK.get(b.code) : 999
if (ra !== rb) return ra - rb
return a.code.localeCompare(b.code)
})
}
/** "USD 美元" / 没中文名时只显示代码 */
export function currencyLabel(code) {
const name = CURRENCY_NAMES[code]
return name ? `${code} ${name}` : code
}
/**
* 交叉汇率:1 from = ? to
* rates 以 USD 为基准,所以 rates[to] / rates[from] 就是任意两币之间的汇率。
*/
export function crossRate(rates, from, to) {
if (!rates) return null
const rf = Number(rates[from])
const rt = Number(rates[to])
if (!Number.isFinite(rf) || !Number.isFinite(rt) || rf <= 0) return null
return rt / rf
}
/**
* 金额显示:汇率结果的有效位数差别很大
* (1 CNY ≈ 0.00004 USD,1 USD ≈ 25000 VND),
* 固定两位小数会让小额变 0、大额全是没意义的零,所以按数量级取精度。
*/
export function formatAmount(value) {
if (!Number.isFinite(value)) return ''
const abs = Math.abs(value)
let digits
if (abs === 0) digits = 2
else if (abs >= 1000) digits = 2
else if (abs >= 1) digits = 4
else if (abs >= 0.001) digits = 6
else digits = 8
return value.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: digits })
}
/** 汇率本身用 6 位有效数字展示:0.000131 这种小汇率也能看清。 */
export function formatRate(value) {
if (!Number.isFinite(value)) return ''
if (value >= 1000) return value.toLocaleString('zh-CN', { maximumFractionDigits: 2 })
if (value >= 1) return value.toLocaleString('zh-CN', { maximumFractionDigits: 4 })
return value.toPrecision(4)
}
/** 把输入框里的字符串转成数字;空串、半截输入("1.")都返回 null 而不是崩掉。 */
export function parseAmount(text) {
const cleaned = String(text ?? '').replace(/[,\s]/g, '')
if (!cleaned) return null
const num = Number(cleaned)
return Number.isFinite(num) ? num : null
}