mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
feat: jalali calendar support and date formatting fixes
- Wire useDatepicker into IntlUtil and switch jalalian display locale to fa-IR for clean "1405/07/03 12:00:00" output (drops the awkward "AP" era suffix that "<lang>-u-ca-persian" produced) - Drop in persian-calendar-suite for the jalali date picker, with a light/dark/ultra theme map and CSS overrides so the inline-styled input stays readable and bg matches the surrounding container - Force LTR on the picker input so "1405/03/07 00:00" reads naturally - Pass calendar setting through ClientInfoModal, ClientsPage Duration tooltip, and ClientFormModal's expiry picker - Heuristic toMs() in ClientInfoModal so GORM's autoUpdateTime seconds render as a real date instead of "1348/11/01" - Persist UpdatedAt on the ClientRecord row in client_service.Update; previously only the inbound settings JSON was bumped, so the panel never saw a fresh updated_at after editing a client
This commit is contained in:
parent
b72212bbb7
commit
f929ea4b14
10 changed files with 211 additions and 65 deletions
80
frontend/package-lock.json
generated
80
frontend/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "3x-ui-frontend",
|
||||
"version": "0.0.3",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "3x-ui-frontend",
|
||||
"version": "0.0.3",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
"dayjs": "^1.11.20",
|
||||
"i18next": "^26.2.0",
|
||||
"otpauth": "^9.5.1",
|
||||
"persian-calendar-suite": "^1.5.5",
|
||||
"qs": "^6.15.2",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
|
|
@ -82,18 +83,6 @@
|
|||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/cssinjs/node_modules/@emotion/hash": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ant-design/cssinjs/node_modules/@emotion/unitless": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
|
||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ant-design/fast-color": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz",
|
||||
|
|
@ -530,6 +519,18 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/hash": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/unitless": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
|
||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||
|
|
@ -2271,21 +2272,6 @@
|
|||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd/node_modules/compute-scroll-into-view": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
|
||||
"integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/antd/node_modules/scroll-into-view-if-needed": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
|
||||
"integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"compute-scroll-into-view": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
|
|
@ -2460,6 +2446,12 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/compute-scroll-into-view": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
|
||||
"integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
|
|
@ -2558,9 +2550,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.360",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.360.tgz",
|
||||
"integrity": "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==",
|
||||
"version": "1.5.361",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
|
||||
"integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
|
|
@ -3666,9 +3658,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.45",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.45.tgz",
|
||||
"integrity": "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==",
|
||||
"version": "2.0.46",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
|
||||
"integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -3769,6 +3761,15 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/persian-calendar-suite": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/persian-calendar-suite/-/persian-calendar-suite-1.5.5.tgz",
|
||||
"integrity": "sha512-KJSzN9q7MZKhfkm97X/j+nD6L0AQ5coUq/B7PpIklXAvRjkALwiV+KmYG0pfr546EQxO9l4fBwE7R1HPI3yT7w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -3956,6 +3957,15 @@
|
|||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scroll-into-view-if-needed": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
|
||||
"integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"compute-scroll-into-view": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "3x-ui-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.3",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).",
|
||||
"engines": {
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
"dayjs": "^1.11.20",
|
||||
"i18next": "^26.2.0",
|
||||
"otpauth": "^9.5.1",
|
||||
"persian-calendar-suite": "^1.5.5",
|
||||
"qs": "^6.15.2",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
|
|
|
|||
35
frontend/src/components/DateTimePicker.css
Normal file
35
frontend/src/components/DateTimePicker.css
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
.jdp-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jdp-wrap > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jdp-wrap input {
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
|
||||
.jdp-dark .jdp-wrap input,
|
||||
.jdp-dark input {
|
||||
color: rgba(255, 255, 255, 0.88) !important;
|
||||
background-color: #23252b !important;
|
||||
}
|
||||
|
||||
.jdp-ultra .jdp-wrap input,
|
||||
.jdp-ultra input {
|
||||
color: rgba(255, 255, 255, 0.88) !important;
|
||||
background-color: #101013 !important;
|
||||
}
|
||||
|
||||
.jdp-dark input::placeholder,
|
||||
.jdp-ultra input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.30) !important;
|
||||
}
|
||||
|
||||
.jdp-disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
|
@ -1,5 +1,12 @@
|
|||
import { useMemo } from 'react';
|
||||
import { DatePicker } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import { PersianDateTimePicker } from 'persian-calendar-suite';
|
||||
|
||||
import { useDatepicker } from '@/hooks/useDatepicker';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import './DateTimePicker.css';
|
||||
|
||||
interface DateTimePickerProps {
|
||||
value: Dayjs | null;
|
||||
|
|
@ -10,6 +17,33 @@ interface DateTimePickerProps {
|
|||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const LIGHT_THEME = {
|
||||
primaryColor: '#1677ff',
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#d9d9d9',
|
||||
hoverColor: 'rgba(22, 119, 255, 0.10)',
|
||||
selectedTextColor: '#ffffff',
|
||||
textColor: 'rgba(0, 0, 0, 0.88)',
|
||||
};
|
||||
|
||||
const DARK_THEME = {
|
||||
primaryColor: '#1677ff',
|
||||
backgroundColor: '#23252b',
|
||||
borderColor: 'rgba(255, 255, 255, 0.12)',
|
||||
hoverColor: 'rgba(22, 119, 255, 0.18)',
|
||||
selectedTextColor: '#ffffff',
|
||||
textColor: 'rgba(255, 255, 255, 0.88)',
|
||||
};
|
||||
|
||||
const ULTRA_DARK_THEME = {
|
||||
primaryColor: '#1677ff',
|
||||
backgroundColor: '#101013',
|
||||
borderColor: 'rgba(255, 255, 255, 0.08)',
|
||||
hoverColor: 'rgba(22, 119, 255, 0.16)',
|
||||
selectedTextColor: '#ffffff',
|
||||
textColor: 'rgba(255, 255, 255, 0.88)',
|
||||
};
|
||||
|
||||
export default function DateTimePicker({
|
||||
value,
|
||||
onChange,
|
||||
|
|
@ -18,6 +52,38 @@ export default function DateTimePicker({
|
|||
placeholder = '',
|
||||
disabled = false,
|
||||
}: DateTimePickerProps) {
|
||||
const { datepicker } = useDatepicker();
|
||||
const { isDark, isUltra } = useTheme();
|
||||
|
||||
const persianTheme = useMemo(() => {
|
||||
if (isUltra) return ULTRA_DARK_THEME;
|
||||
if (isDark) return DARK_THEME;
|
||||
return LIGHT_THEME;
|
||||
}, [isDark, isUltra]);
|
||||
|
||||
if (datepicker === 'jalalian') {
|
||||
return (
|
||||
<div className={`jdp-wrap${isDark ? ' jdp-dark' : ''}${isUltra ? ' jdp-ultra' : ''}${disabled ? ' jdp-disabled' : ''}`}>
|
||||
<PersianDateTimePicker
|
||||
value={value ? value.valueOf() : null}
|
||||
onChange={(next: number | string | null) => {
|
||||
if (next == null || next === '') {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
const ms = typeof next === 'number' ? next : Number(next);
|
||||
if (Number.isFinite(ms)) onChange(dayjs(ms));
|
||||
}}
|
||||
showTime={showTime}
|
||||
outputFormat="timestamp"
|
||||
persianNumbers
|
||||
rtlCalendar
|
||||
theme={persianTheme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
value={value}
|
||||
|
|
|
|||
36
frontend/src/env.d.ts
vendored
36
frontend/src/env.d.ts
vendored
|
|
@ -27,3 +27,39 @@ interface Window {
|
|||
X_UI_CUR_VER?: string;
|
||||
__SUB_PAGE_DATA__?: SubPageData;
|
||||
}
|
||||
|
||||
declare module 'persian-calendar-suite' {
|
||||
import type { ComponentType, ReactNode } from 'react';
|
||||
|
||||
type DateInput = string | number | null;
|
||||
type OutputFormat = 'iso' | 'shamsi' | 'gregorian' | 'hijri' | 'timestamp';
|
||||
|
||||
interface PersianDateTimePickerProps {
|
||||
value?: DateInput;
|
||||
onChange?: (value: number | string | null) => void;
|
||||
defaultValue?: string | number | 'now' | null;
|
||||
showTime?: boolean;
|
||||
minuteStep?: number;
|
||||
outputFormat?: OutputFormat;
|
||||
showFooter?: boolean;
|
||||
theme?: Record<string, unknown>;
|
||||
disabledHours?: number[];
|
||||
minDate?: string | Date | null;
|
||||
maxDate?: string | Date | null;
|
||||
enabledDates?: string[] | null;
|
||||
disabledDates?: string[] | null;
|
||||
disabledWeekDays?: number[];
|
||||
persianNumbers?: boolean;
|
||||
rtlCalendar?: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const PersianDateTimePicker: ComponentType<PersianDateTimePickerProps>;
|
||||
export const PersianCalendar: ComponentType<Record<string, unknown>>;
|
||||
export const PersianDateRangePicker: ComponentType<Record<string, unknown>>;
|
||||
export const PersianTimePicker: ComponentType<Record<string, unknown>>;
|
||||
export const PersianTimeline: ComponentType<Record<string, unknown>>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
|
|||
import {
|
||||
Button,
|
||||
Col,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
|
|
@ -19,6 +18,7 @@ import dayjs from 'dayjs';
|
|||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import { HttpUtil, RandomUtil } from '@/utils';
|
||||
import DateTimePicker from '@/components/DateTimePicker';
|
||||
import { TLS_FLOW_CONTROL } from '@/models/inbound';
|
||||
import type { ClientRecord, InboundOption } from '@/hooks/useClients';
|
||||
import './ClientFormModal.css';
|
||||
|
|
@ -416,11 +416,9 @@ export default function ClientFormModal({
|
|||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item label={t('pages.clients.expiryTime')}>
|
||||
<DatePicker
|
||||
<DateTimePicker
|
||||
value={form.expiryDate}
|
||||
onChange={(d) => update('expiryDate', d || null)}
|
||||
showTime
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Button, Divider, Modal, Tag, Tooltip, message } from 'antd';
|
|||
import { CopyOutlined } from '@ant-design/icons';
|
||||
|
||||
import { ClipboardManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils';
|
||||
import { useDatepicker } from '@/hooks/useDatepicker';
|
||||
import type { ClientRecord, InboundOption } from '@/hooks/useClients';
|
||||
import './ClientInfoModal.css';
|
||||
|
||||
|
|
@ -30,16 +31,6 @@ interface ApiMsg<T = unknown> {
|
|||
|
||||
const DEFAULT_SUB: SubSettings = { enable: false, subURI: '', subJsonURI: '', subJsonEnable: false };
|
||||
|
||||
function expiryLabel(ts?: number) {
|
||||
if (!ts || ts <= 0) return '∞';
|
||||
return IntlUtil.formatDate(ts);
|
||||
}
|
||||
|
||||
function dateLabel(ts?: number) {
|
||||
if (!ts || ts <= 0) return '-';
|
||||
return IntlUtil.formatDate(ts);
|
||||
}
|
||||
|
||||
export default function ClientInfoModal({
|
||||
open,
|
||||
client,
|
||||
|
|
@ -48,6 +39,9 @@ export default function ClientInfoModal({
|
|||
subSettings = DEFAULT_SUB,
|
||||
onOpenChange,
|
||||
}: ClientInfoModalProps) {
|
||||
const { datepicker } = useDatepicker();
|
||||
const expiryLabel = (ts?: number) => (!ts || ts <= 0 ? '∞' : IntlUtil.formatDate(ts, datepicker));
|
||||
const dateLabel = (ts?: number) => (!ts || ts <= 0 ? '-' : IntlUtil.formatDate(ts, datepicker));
|
||||
const { t } = useTranslation();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [links, setLinks] = useState<string[]>([]);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import { useTheme } from '@/hooks/useTheme';
|
|||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useWebSocket } from '@/hooks/useWebSocket';
|
||||
import { useClients } from '@/hooks/useClients';
|
||||
import { useDatepicker } from '@/hooks/useDatepicker';
|
||||
import type { ClientRecord, InboundOption } from '@/hooks/useClients';
|
||||
import AppSidebar from '@/components/AppSidebar';
|
||||
import CustomStatistic from '@/components/CustomStatistic';
|
||||
|
|
@ -86,6 +87,7 @@ function readFilterState(): FilterState {
|
|||
export default function ClientsPage() {
|
||||
const { t } = useTranslation();
|
||||
const { isDark, isUltra, antdThemeConfig } = useTheme();
|
||||
const { datepicker } = useDatepicker();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [modal, modalContextHolder] = Modal.useModal();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
|
|
@ -296,7 +298,7 @@ export default function ClientsPage() {
|
|||
const days = Math.round(row.expiryTime / -86400000);
|
||||
return `${t('pages.clients.delayedStart')}: ${days}d`;
|
||||
}
|
||||
return IntlUtil.formatDate(row.expiryTime);
|
||||
return IntlUtil.formatDate(row.expiryTime, datepicker);
|
||||
}
|
||||
|
||||
function expiryRelative(row: ClientRecord) {
|
||||
|
|
|
|||
|
|
@ -911,24 +911,22 @@ export class FileManager {
|
|||
}
|
||||
|
||||
export class IntlUtil {
|
||||
// When `calendar` is "jalalian", append the BCP-47 calendar extension
|
||||
// so Intl renders the date in the Persian (Jalali/Shamsi) calendar
|
||||
// regardless of the UI language. Without it, only locales that
|
||||
// default to Persian (e.g. fa-IR) would show Jalali; en-US/ru/etc.
|
||||
// would keep showing Gregorian.
|
||||
// For Jalali display, always use fa-IR locale (its default calendar
|
||||
// is Persian) so we get a clean "1405/07/03 12:00:00" format with
|
||||
// Persian digits, without the awkward "AP" era suffix that appears
|
||||
// when other locales force `-u-ca-persian`.
|
||||
static formatDate(date, calendar = "gregorian") {
|
||||
const language = LanguageManager.getLanguage()
|
||||
const locale = calendar === "jalalian"
|
||||
? `${language}-u-ca-persian`
|
||||
: language
|
||||
const locale = calendar === "jalalian" ? "fa-IR" : language
|
||||
|
||||
let intlOptions = {
|
||||
const intlOptions = {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric"
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
}
|
||||
|
||||
const intl = new Intl.DateTimeFormat(
|
||||
|
|
|
|||
|
|
@ -604,6 +604,12 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
|
|||
needRestart = true
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.GetDB().Model(&model.ClientRecord{}).
|
||||
Where("id = ?", id).
|
||||
Update("updated_at", updated.UpdatedAt).Error; err != nil {
|
||||
return needRestart, err
|
||||
}
|
||||
return needRestart, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue