feat(frontend): jalali calendar + drop legacy moment-jalali

- Wire Calendar Type setting to a real Jalali datepicker via
  vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
  dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
  (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
  white); fix invisible disabled days, SVG arrow fills, popup clipping
  via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
  the legacy global was undefined under ESM and broke the inbounds list
  whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
  assets — replaced by the Vue 3 picker

Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei 2026-05-09 00:17:25 +02:00
parent 085a12e469
commit cbb35f73ed
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
15 changed files with 568 additions and 94 deletions

View file

@ -17,7 +17,8 @@
"qrious": "^4.0.2",
"qs": "^6.13.1",
"vue": "^3.5.13",
"vue-i18n": "^11.1.4"
"vue-i18n": "^11.1.4",
"vue3-persian-datetime-picker": "^1.2.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.6",
@ -961,8 +962,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/boolbase": {
"version": "1.0.0",
@ -974,7 +974,6 @@
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -1069,8 +1068,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/core-js": {
"version": "3.49.0",
@ -1549,6 +1547,11 @@
"node": ">= 6"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -1606,6 +1609,26 @@
"node": ">= 0.4"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@ -1720,6 +1743,21 @@
"node": ">=0.8.19"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -1755,6 +1793,11 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/jalaali-js": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz",
"integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A=="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -2142,7 +2185,6 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -2158,6 +2200,28 @@
"node": "*"
}
},
"node_modules/moment-jalaali": {
"version": "0.9.6",
"resolved": "https://registry.npmjs.org/moment-jalaali/-/moment-jalaali-0.9.6.tgz",
"integrity": "sha512-v8wXjQplvk5ez+sUqgsWIrafwIf1BEXXvzTYwsg1wHcqh27nSgKPCJ6FnZRrCz03MoNyB9N31L0oms+vE8Rq7g==",
"dependencies": {
"jalaali-js": "^1.1.0",
"moment": "^2.22.2",
"moment-timezone": "^0.5.21",
"rimraf": "^3.0.2"
}
},
"node_modules/moment-timezone": {
"version": "0.5.48",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
"dependencies": {
"moment": "^2.29.4"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -2215,6 +2279,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -2294,6 +2366,14 @@
"node": ">=8"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -2419,6 +2499,21 @@
"node": ">=4"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.18",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz",
@ -2879,6 +2974,14 @@
"vue": "^3.0.0"
}
},
"node_modules/vue3-persian-datetime-picker": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/vue3-persian-datetime-picker/-/vue3-persian-datetime-picker-1.2.2.tgz",
"integrity": "sha512-d7nkj5vgtUvEXZboSdRmP1uwBfXvXgXqdvsOOMQb34jiMZU/aBDrTYWTEe1N+XKF9pvTTJn8Rws9ttJmyhK/hw==",
"dependencies": {
"moment-jalaali": "^0.9.4"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
@ -2911,6 +3014,11 @@
"node": ">=0.10.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",

View file

@ -20,7 +20,8 @@
"qrious": "^4.0.2",
"qs": "^6.13.1",
"vue": "^3.5.13",
"vue-i18n": "^11.1.4"
"vue-i18n": "^11.1.4",
"vue3-persian-datetime-picker": "^1.2.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.6",

View file

@ -0,0 +1,383 @@
<script setup>
import { computed } from 'vue';
import dayjs from 'dayjs';
import PersianDatePicker from 'vue3-persian-datetime-picker';
import { useDatepicker } from '@/composables/useDatepicker.js';
// Drop-in replacement for <a-date-picker> that swaps to a real Jalali
// calendar (vue3-persian-datetime-picker, backed by moment-jalaali)
// when the panel's "Calendar Type" setting is `jalalian`.
//
// The v-model contract matches AD-Vue: the parent works with a dayjs
// object (or null). For the persian picker we serialize to/from the
// `YYYY-MM-DD HH:mm:ss` string it expects so callers don't need to
// know which renderer is active.
const props = defineProps({
value: { type: [Object, null], default: null },
showTime: { type: Boolean, default: true },
format: { type: String, default: 'YYYY-MM-DD HH:mm:ss' },
placeholder: { type: String, default: '' },
disabled: { type: Boolean, default: false },
});
const emit = defineEmits(['update:value']);
const { datepicker } = useDatepicker();
const isJalali = computed(() => datepicker.value === 'jalalian');
const ISO_FORMAT = 'YYYY-MM-DD HH:mm:ss';
// Persian picker's display format `j` tokens come from moment-jalaali
// and render Jalali year/month/day.
const persianDisplayFormat = computed(() =>
props.showTime ? 'jYYYY/jMM/jDD HH:mm:ss' : 'jYYYY/jMM/jDD',
);
// Persian picker stores the date as a Gregorian string in the format
// it was given via `format`. We normalize on `YYYY-MM-DD HH:mm:ss` so
// dayjs(...) round-trips cleanly.
const stringValue = computed({
get() {
const v = props.value;
if (!v) return '';
return dayjs.isDayjs(v) ? v.format(ISO_FORMAT) : dayjs(v).format(ISO_FORMAT);
},
set(next) {
if (!next) {
emit('update:value', null);
return;
}
const parsed = dayjs(next, ISO_FORMAT);
emit('update:value', parsed.isValid() ? parsed : null);
},
});
function onAntChange(next) {
emit('update:value', next || null);
}
</script>
<template>
<PersianDatePicker
v-if="isJalali"
v-model="stringValue"
:format="ISO_FORMAT"
:display-format="persianDisplayFormat"
:placeholder="placeholder"
:disabled="disabled"
color="#1677ff"
auto-submit
append-to="body"
input-class="ant-input persian-datepicker-input"
class="jalali-datepicker"
/>
<a-date-picker
v-else
:value="value"
:show-time="showTime ? { format: 'HH:mm:ss' } : false"
:format="format"
:placeholder="placeholder"
:disabled="disabled"
:style="{ width: '100%' }"
@update:value="onAntChange"
/>
</template>
<style scoped>
.jalali-datepicker {
width: 100%;
}
</style>
<!-- Theme overrides for the picker. AD-Vue 4 doesn't expose CSS variables
by default (its tokens live in JS), so we hardcode hexes per theme
class `body.dark` for the navy theme, `[data-theme="ultra-dark"]`
for the neutral ultra-dark variant. The popup stays inside the
wrapper's subtree (no teleport) so global selectors reach it cleanly. -->
<style>
/* ===== Light (default) =================================================== */
.persian-datepicker-input {
width: 100%;
box-sizing: border-box;
padding: 4px 11px;
font-size: 14px;
border: 1px solid #d9d9d9;
border-radius: 6px;
background: #fff;
color: rgba(0, 0, 0, 0.88);
transition: border-color 0.2s, box-shadow 0.2s;
}
.persian-datepicker-input:hover {
border-color: #4096ff;
}
.persian-datepicker-input:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
outline: none;
}
/* Light theme keeps the picker's brand-blue calendar button (set via
* inline style on .vpd-icon-btn) only its border + corner radius are
* normalized so it sits flush with the input. Dark/ultra-dark themes
* below override the inline blue so the control matches the form. */
.vpd-main .vpd-icon-btn {
color: #fff;
border: 1px solid transparent;
border-radius: 6px 0 0 6px;
}
/* Match the input's left edge (no rounded left, no double border at the
* seam) so it sits flush against the icon-btn. */
.persian-datepicker-input {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.vpd-main .vpd-clear-btn {
color: rgba(0, 0, 0, 0.45);
background: transparent;
}
.vpd-main .vpd-content {
background: #fff;
color: rgba(0, 0, 0, 0.88);
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(5, 5, 5, 0.06);
border-radius: 8px;
overflow: hidden;
}
.vpd-main .vpd-header {
background: #1677ff;
color: #fff;
border-radius: 8px 8px 0 0;
}
.vpd-main .vpd-header .vpd-year-label,
.vpd-main .vpd-header .vpd-date,
.vpd-main .vpd-header .vpd-locales li {
color: #fff;
}
.vpd-main .vpd-body {
background: #fff;
color: rgba(0, 0, 0, 0.88);
}
.vpd-main .vpd-body .vpd-month-label,
.vpd-main .vpd-body .vpd-month-label > span {
color: rgba(0, 0, 0, 0.88);
}
.vpd-main .vpd-body .vpd-week,
.vpd-main .vpd-body .vpd-weekday {
color: rgba(0, 0, 0, 0.55);
}
.vpd-main .vpd-body .vpd-controls .vpd-next,
.vpd-main .vpd-body .vpd-controls .vpd-prev {
color: rgba(0, 0, 0, 0.65);
}
/* The picker's <arrow> component renders an inline SVG with a hardcoded
* `fill="#000"` attribute. Override the path fill via CSS so the arrow
* is visible in every theme. */
.vpd-main .vpd-next svg path,
.vpd-main .vpd-prev svg path {
fill: rgba(0, 0, 0, 0.65);
}
.vpd-main .vpd-body .vpd-controls .vpd-next:hover svg path,
.vpd-main .vpd-body .vpd-controls .vpd-prev:hover svg path {
fill: #1677ff;
}
/* The picker paints disabled days as `darken(#fff, 20%)` (~#cccccc) which
* is invisible on white and dark themes alike. Reset the day text color
* across all states so days are always readable. */
.vpd-main .vpd-day,
.vpd-main .vpd-day .vpd-day-text {
color: rgba(0, 0, 0, 0.88) !important;
}
.vpd-main .vpd-day[disabled='true'],
.vpd-main .vpd-day[disabled='true'] .vpd-day-text {
color: rgba(0, 0, 0, 0.25) !important;
}
.vpd-main .vpd-day:not([disabled='true']):hover .vpd-day-text,
.vpd-main .vpd-day.vpd-selected .vpd-day-text {
color: #fff !important;
}
.vpd-main .vpd-actions button {
color: rgba(0, 0, 0, 0.88);
background: transparent;
}
.vpd-main .vpd-actions button:hover {
background: rgba(0, 0, 0, 0.04);
color: #1677ff;
}
.vpd-main .vpd-addon-list,
.vpd-main .vpd-addon-list-content {
background: #fff;
color: rgba(0, 0, 0, 0.88);
}
.vpd-main .vpd-addon-list-item {
color: rgba(0, 0, 0, 0.88);
border-color: #fff;
}
.vpd-main .vpd-addon-list-item.vpd-selected,
.vpd-main .vpd-addon-list-item:hover {
background: rgba(0, 0, 0, 0.04);
}
.vpd-main .vpd-close-addon {
color: rgba(0, 0, 0, 0.65);
background: rgba(0, 0, 0, 0.06);
}
/* ===== Dark (navy) ======================================================= */
body.dark .persian-datepicker-input {
background: #142340;
border-color: #1f3358;
color: rgba(255, 255, 255, 0.88);
}
body.dark .persian-datepicker-input:hover {
border-color: #4096ff;
}
body.dark .persian-datepicker-input:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.18);
}
body.dark .vpd-main .vpd-icon-btn {
background: rgba(255, 255, 255, 0.04) !important;
border: 1px solid #1f3358 !important;
border-right: none !important;
border-radius: 6px 0 0 6px !important;
color: rgba(255, 255, 255, 0.75) !important;
}
body.dark .vpd-main .vpd-content {
background: #1a2c4d;
color: rgba(255, 255, 255, 0.88);
border-color: rgba(255, 255, 255, 0.08);
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.32),
0 3px 6px -4px rgba(0, 0, 0, 0.48),
0 9px 28px 8px rgba(0, 0, 0, 0.2);
}
body.dark .vpd-main .vpd-body {
background: #1a2c4d;
color: rgba(255, 255, 255, 0.88);
}
body.dark .vpd-main .vpd-body .vpd-month-label,
body.dark .vpd-main .vpd-body .vpd-month-label > span {
color: rgba(255, 255, 255, 0.88);
}
body.dark .vpd-main .vpd-body .vpd-week,
body.dark .vpd-main .vpd-body .vpd-weekday {
color: rgba(255, 255, 255, 0.55);
}
body.dark .vpd-main .vpd-body .vpd-controls .vpd-next,
body.dark .vpd-main .vpd-body .vpd-controls .vpd-prev {
color: rgba(255, 255, 255, 0.65);
}
body.dark .vpd-main .vpd-next svg path,
body.dark .vpd-main .vpd-prev svg path {
fill: rgba(255, 255, 255, 0.75);
}
body.dark .vpd-main .vpd-body .vpd-controls .vpd-next:hover svg path,
body.dark .vpd-main .vpd-body .vpd-controls .vpd-prev:hover svg path {
fill: #4096ff;
}
body.dark .vpd-main .vpd-day,
body.dark .vpd-main .vpd-day .vpd-day-text {
color: rgba(255, 255, 255, 0.88) !important;
}
body.dark .vpd-main .vpd-day[disabled='true'],
body.dark .vpd-main .vpd-day[disabled='true'] .vpd-day-text {
color: rgba(255, 255, 255, 0.25) !important;
}
body.dark .vpd-main .vpd-actions button {
color: rgba(255, 255, 255, 0.88);
}
body.dark .vpd-main .vpd-actions button:hover {
background: rgba(255, 255, 255, 0.06);
}
body.dark .vpd-main .vpd-addon-list,
body.dark .vpd-main .vpd-addon-list-content {
background: #1a2c4d;
color: rgba(255, 255, 255, 0.88);
}
body.dark .vpd-main .vpd-addon-list-item {
color: rgba(255, 255, 255, 0.88);
border-color: transparent;
}
body.dark .vpd-main .vpd-addon-list-item.vpd-selected,
body.dark .vpd-main .vpd-addon-list-item:hover {
background: rgba(255, 255, 255, 0.06);
}
body.dark .vpd-main .vpd-close-addon {
color: rgba(255, 255, 255, 0.65);
background: rgba(255, 255, 255, 0.08);
}
/* ===== Ultra-dark (neutral black) ======================================= */
html[data-theme='ultra-dark'] .persian-datepicker-input {
background: #0a0a0a;
border-color: #303030;
color: rgba(255, 255, 255, 0.88);
}
html[data-theme='ultra-dark'] .vpd-main .vpd-icon-btn {
background: rgba(255, 255, 255, 0.04) !important;
border: 1px solid #303030 !important;
border-right: none !important;
border-radius: 6px 0 0 6px !important;
color: rgba(255, 255, 255, 0.75) !important;
}
html[data-theme='ultra-dark'] .vpd-main .vpd-content {
background: #141414;
color: rgba(255, 255, 255, 0.88);
border-color: rgba(255, 255, 255, 0.08);
}
html[data-theme='ultra-dark'] .vpd-main .vpd-body {
background: #141414;
}
html[data-theme='ultra-dark'] .vpd-main .vpd-addon-list,
html[data-theme='ultra-dark'] .vpd-main .vpd-addon-list-content {
background: #141414;
}
</style>

View file

@ -0,0 +1,45 @@
// Module-scoped reactive ref for the panel's "Calendar Type" setting.
// Loaded from /panel/setting/defaultSettings on first use, so any
// component (modals, inbound forms, future pages) can read the same
// value without prop-drilling and without re-fetching.
//
// useInbounds (which already reads defaultSettings for its own state)
// calls setDatepicker() after its fetch so we don't issue a second
// HTTP round-trip on the inbounds page.
import { readonly, ref } from 'vue';
import { HttpUtil } from '@/utils';
const datepicker = ref('gregorian');
let fetched = false;
let pending = null;
async function loadOnce() {
if (fetched) return;
if (pending) {
await pending;
return;
}
pending = (async () => {
try {
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (msg?.success) {
datepicker.value = msg.obj?.datepicker || 'gregorian';
}
} finally {
fetched = true;
pending = null;
}
})();
await pending;
}
export function setDatepicker(value) {
fetched = true;
datepicker.value = value || 'gregorian';
}
export function useDatepicker() {
loadOnce();
return { datepicker: readonly(datepicker) };
}

View file

@ -1,3 +1,4 @@
import dayjs from 'dayjs';
import { ObjectUtil, NumberFormatter, SizeFormatter } from '@/utils';
import { Inbound, Protocols } from './inbound.js';
@ -78,7 +79,7 @@ export class DBInbound {
if (this.expiryTime === 0) {
return null;
}
return moment(this.expiryTime);
return dayjs(this.expiryTime);
}
set _expiryTime(t) {

View file

@ -1,3 +1,4 @@
import dayjs from 'dayjs';
import { ObjectUtil, RandomUtil, Base64, NumberFormatter, SizeFormatter, Wireguard } from '@/utils';
export const Protocols = {
@ -2523,7 +2524,7 @@ Inbound.ClientBase = class extends XrayCommonClass {
if (this.expiryTime < 0) {
return this.expiryTime / -86400000;
}
return moment(this.expiryTime);
return dayjs(this.expiryTime);
}
set _expiryTime(t) {

View file

@ -13,6 +13,7 @@ import {
USERS_SECURITY,
TLS_FLOW_CONTROL,
} from '@/models/inbound.js';
import DateTimePicker from '@/components/DateTimePicker.vue';
// Bulk-add up to 500 clients in one go. The legacy panel offers five
// generation modes this component preserves them all:
@ -250,8 +251,7 @@ async function submit() {
<a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate')
}}</a-tooltip>
</template>
<a-date-picker v-model:value="expiryDate" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:style="{ width: '100%' }" />
<DateTimePicker v-model:value="expiryDate" />
</a-form-item>
<a-form-item v-if="form.expiryTime !== 0">

View file

@ -11,6 +11,7 @@ import {
ColorUtils,
} from '@/utils';
import { Inbound, Protocols, USERS_SECURITY, TLS_FLOW_CONTROL } from '@/models/inbound.js';
import DateTimePicker from '@/components/DateTimePicker.vue';
const { t } = useI18n();
@ -363,8 +364,7 @@ const title = computed(() =>
<a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate')
}}</a-tooltip>
</template>
<a-date-picker v-model:value="expiryDate" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:style="{ width: '100%' }" />
<DateTimePicker v-model:value="expiryDate" />
<a-tag v-if="mode === 'edit' && isExpired" color="red">{{ t('depleted') }}</a-tag>
</a-form-item>

View file

@ -30,6 +30,7 @@ import {
} from '@/models/inbound.js';
import { DBInbound } from '@/models/dbinbound.js';
import FinalMaskForm from '@/components/FinalMaskForm.vue';
import DateTimePicker from '@/components/DateTimePicker.vue';
const { t } = useI18n();
@ -572,8 +573,7 @@ watch(
<a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate')
}}</a-tooltip>
</template>
<a-date-picker v-model:value="expiryDate" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:style="{ width: '100%' }" />
<DateTimePicker v-model:value="expiryDate" />
</a-form-item>
</a-form>
</a-tab-pane>
@ -667,8 +667,7 @@ watch(
</a-form-item>
<a-form-item label="Expiry">
<a-date-picker v-model:value="clientExpiryDate" :show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss" :style="{ width: '100%' }" />
<DateTimePicker v-model:value="clientExpiryDate" />
</a-form-item>
</a-form>
</a-collapse-panel>

View file

@ -9,6 +9,7 @@ import { computed, ref, shallowRef } from 'vue';
import { HttpUtil, ObjectUtil } from '@/utils';
import { DBInbound } from '@/models/dbinbound.js';
import { Protocols } from '@/models/inbound.js';
import { setDatepicker } from '@/composables/useDatepicker.js';
const ONLINE_GRACE_MS = 60_000;
@ -146,6 +147,9 @@ export function useInbounds() {
pageSize.value = s.pageSize ?? 0;
remarkModel.value = s.remarkModel || '-ieo';
datepicker.value = s.datepicker || 'gregorian';
// Mirror into the global composable so date-pickers in modals can
// pick the right calendar without re-fetching the settings.
setDatepicker(datepicker.value);
ipLimitEnable.value = !!s.ipLimitEnable;
}

View file

@ -127,6 +127,14 @@ export default defineConfig({
if (id.includes('dayjs')) return 'vendor-dayjs';
if (id.includes('qrious')) return 'vendor-qrious';
if (id.includes('axios')) return 'vendor-axios';
// The persian datepicker pulls in moment + moment-jalaali; bundle
// the trio together so unrelated pages don't pay the cost.
if (
id.includes('vue3-persian-datetime-picker')
|| id.includes('moment-jalaali')
|| id.includes('jalaali-js')
|| id.includes('/node_modules/moment/')
) return 'vendor-jalali';
return 'vendor';
},
},

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,73 +0,0 @@
{{define "component/persianDatepickerTemplate"}}
<template>
<div>
<a-input :value="value" type="text" v-model="date" data-jdp class="persian-datepicker"
@input="$emit('input', convertToGregorian($event.target.value)); jalaliDatepicker.hide();"
:placeholder="placeholder">
<template #addonAfter>
<a-icon type="calendar" :style="{ fontSize: '14px', opacity: '0.5' }" />
</template>
</a-input>
</div>
</template>
{{end}}
{{define "component/aPersianDatepicker"}}
<link rel="stylesheet" href="{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.css?{{ .cur_ver }}" />
<script src="{{ .base_path }}assets/moment/moment-jalali.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.js?{{ .cur_ver }}"></script>
<script>
const persianDatepicker = {};
Vue.component('a-persian-datepicker', {
props: {
'format': {
type: undefined,
required: false,
},
'value': {
type: String,
required: false,
},
'placeholder': {
type: String,
required: false,
},
},
template: `{{template "component/persianDatepickerTemplate" .}}`,
data() {
return {
date: '',
persianDatepicker,
};
},
watch: {
value: function(date) {
this.date = this.convertToJalalian(date)
}
},
mounted() {
this.date = this.convertToJalalian(this.value)
this.listenToDatepicker()
},
methods: {
convertToGregorian(date) {
return date ? moment(moment(date, 'jYYYY/jMM/jDD HH:mm:ss').format('YYYY-MM-DD HH:mm:ss')) :
null
},
convertToJalalian(date) {
return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null
},
listenToDatepicker() {
jalaliDatepicker.startWatch({
time: true,
zIndex: '9999',
hideAfterChange: true,
useDropDownYears: false,
changeMonthRotateYear: true,
});
},
}
});
</script>
{{end}}