This commit is contained in:
Baobhan Sith
2025-04-26 20:51:31 +08:00
parent 70749a2112
commit 2544bc9646
9 changed files with 126 additions and 35 deletions
+11
View File
@@ -2900,6 +2900,15 @@
"url": "https://github.com/sponsors/kossnocorp" "url": "https://github.com/sponsors/kossnocorp"
} }
}, },
"node_modules/date-fns-tz": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
"license": "MIT",
"peerDependencies": {
"date-fns": "^3.0.0 || ^4.0.0"
}
},
"node_modules/de-indent": { "node_modules/de-indent": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@@ -7534,6 +7543,8 @@
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"axios": "^1.8.4", "axios": "^1.8.4",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^5.1.0", "express": "^5.1.0",
"express-session": "^1.18.1", "express-session": "^1.18.1",
+4 -2
View File
@@ -29,8 +29,10 @@ COPY --from=builder /app/packages/backend/dist ./dist
# --- 添加:复制 locales 目录 --- # --- 添加:复制 locales 目录 ---
COPY --from=builder /app/packages/backend/src/locales ./dist/locales COPY --from=builder /app/packages/backend/src/locales ./dist/locales
# --- 结束添加 --- # --- 结束添加 ---
COPY --from=builder /app/packages/backend/package.json ./package.json # --- 修改:从构建上下文复制 package 文件,以包含新依赖 ---
COPY --from=builder /app/package-lock.json ./package-lock.json COPY packages/backend/package.json ./package.json
COPY package-lock.json ./package-lock.json
# --- 结束修改 ---
RUN npm install --omit=dev --prefer-offline RUN npm install --omit=dev --prefer-offline
+2
View File
@@ -15,6 +15,8 @@
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"axios": "^1.8.4", "axios": "^1.8.4",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^5.1.0", "express": "^5.1.0",
"express-session": "^1.18.1", "express-session": "^1.18.1",
-3
View File
@@ -5,9 +5,6 @@ import fs from 'fs';
// --- 动态确定支持的语言 --- // --- 动态确定支持的语言 ---
const localesDir = path.join(__dirname, 'locales'); const localesDir = path.join(__dirname, 'locales');
// --- 添加调试日志 ---
console.log(`[i18next-debug] Calculated locales directory path: ${localesDir}`);
// --- 结束调试日志 ---
let dynamicSupportedLngs: string[] = []; let dynamicSupportedLngs: string[] = [];
try { try {
const entries = fs.readdirSync(localesDir, { withFileTypes: true }); const entries = fs.readdirSync(localesDir, { withFileTypes: true });
@@ -267,6 +267,7 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
statusMonitorIntervalSeconds: '3', statusMonitorIntervalSeconds: '3',
[SIDEBAR_CONFIG_KEY]: JSON.stringify(defaultSidebarPanesStructure), [SIDEBAR_CONFIG_KEY]: JSON.stringify(defaultSidebarPanesStructure),
[CAPTCHA_CONFIG_KEY]: JSON.stringify(defaultCaptchaSettings), [CAPTCHA_CONFIG_KEY]: JSON.stringify(defaultCaptchaSettings),
timezone: 'UTC', // NEW: 添加时区默认值
}; };
const nowSeconds = Math.floor(Date.now() / 1000); const nowSeconds = Math.floor(Date.now() / 1000);
const sqlInsertOrIgnore = `INSERT OR IGNORE INTO settings (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`; const sqlInsertOrIgnore = `INSERT OR IGNORE INTO settings (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`;
@@ -14,6 +14,7 @@ import * as nodemailer from 'nodemailer';
import Mail from 'nodemailer/lib/mailer'; import Mail from 'nodemailer/lib/mailer';
import i18next, { defaultLng, supportedLngs } from '../i18n'; // Import supportedLngs import i18next, { defaultLng, supportedLngs } from '../i18n'; // Import supportedLngs
import { settingsService } from './settings.service'; import { settingsService } from './settings.service';
import { formatInTimeZone } from 'date-fns-tz'; // NEW: Import timezone formatting
const testSubjectKey = 'testNotification.subject'; const testSubjectKey = 'testNotification.subject';
@@ -269,24 +270,27 @@ export class NotificationService {
console.log(`[通知] 事件触发: ${event}`, details || ''); console.log(`[通知] 事件触发: ${event}`, details || '');
let userLang = defaultLng; let userLang = defaultLng;
let userTimezone = 'UTC'; // NEW: Default timezone
try { try {
const langSetting = await settingsService.getSetting('language'); // Fetch language and timezone settings concurrently
// --- 添加调试日志 --- const [langSetting, timezoneSetting] = await Promise.all([
console.log(`[通知调试] 刚从数据库获取的 langSetting: ${langSetting}`); settingsService.getSetting('language'),
// --- 结束调试日志 --- settingsService.getSetting('timezone') // NEW: Fetch timezone
// --- 添加调试日志 --- ]);
console.log(`[通知调试] Checking langSetting against supportedLngs:`, supportedLngs);
// --- 结束调试日志 ---
if (langSetting && supportedLngs.includes(langSetting)) { if (langSetting && supportedLngs.includes(langSetting)) {
userLang = langSetting; userLang = langSetting;
} }
} catch (error) { // NEW: Validate and set timezone
console.error(`[通知] 获取事件 ${event} 的语言设置时出错:`, error); if (timezoneSetting) {
// Basic validation: Check if it's a non-empty string.
// More robust validation could involve checking against Intl.supportedValuesOf('timeZone')
// but that might be overkill depending on how timezones are set/validated elsewhere.
userTimezone = timezoneSetting;
} }
// --- 添加调试日志 --- } catch (error) {
console.log(`[通知调试] 最终决定使用的 userLang: ${userLang}`); console.error(`[通知] 获取事件 ${event} 的语言或时区设置时出错:`, error); // Modified log
// --- 结束调试日志 --- }
console.log(`[通知] 事件 ${event} 使用语言 '${userLang}'`); console.log(`[通知] 事件 ${event} 使用语言 '${userLang}', 时区 '${userTimezone}'`); // Modified log
const payload: NotificationPayload = { const payload: NotificationPayload = {
event, event,
@@ -305,11 +309,11 @@ export class NotificationService {
const sendPromises = applicableSettings.map(setting => { const sendPromises = applicableSettings.map(setting => {
switch (setting.channel_type) { switch (setting.channel_type) {
case 'webhook': case 'webhook':
return this._sendWebhook(setting, payload, userLang); return this._sendWebhook(setting, payload, userLang, userTimezone); // Pass timezone
case 'email': case 'email':
return this._sendEmail(setting, payload, userLang); return this._sendEmail(setting, payload, userLang, userTimezone); // Pass timezone
case 'telegram': case 'telegram':
return this._sendTelegram(setting, payload, userLang); return this._sendTelegram(setting, payload, userLang, userTimezone); // Pass timezone
default: default:
console.warn(`[通知] 未知渠道类型: ${setting.channel_type} (设置 ID: ${setting.id})`); console.warn(`[通知] 未知渠道类型: ${setting.channel_type} (设置 ID: ${setting.id})`);
return Promise.resolve(); // 如果有一个未知,不要让所有都失败 return Promise.resolve(); // 如果有一个未知,不要让所有都失败
@@ -345,7 +349,7 @@ export class NotificationService {
return rendered; return rendered;
} }
private async _sendWebhook(setting: NotificationSetting, payload: NotificationPayload, userLang: string): Promise<void> { private async _sendWebhook(setting: NotificationSetting, payload: NotificationPayload, userLang: string, userTimezone: string): Promise<void> { // Add userTimezone
const config = setting.config as WebhookConfig; const config = setting.config as WebhookConfig;
if (!config.url) { if (!config.url) {
console.error(`[通知] Webhook 设置 ID ${setting.id} 缺少 URL。`); console.error(`[通知] Webhook 设置 ID ${setting.id} 缺少 URL。`);
@@ -363,7 +367,8 @@ export class NotificationService {
const templateDataWebhook: Record<string, string> = { const templateDataWebhook: Record<string, string> = {
event: translatedPayload.event, event: translatedPayload.event,
eventDisplay: eventDisplayName, // Assuming no markdown needed for webhook eventDisplay: eventDisplayName, // Assuming no markdown needed for webhook
timestamp: new Date(translatedPayload.timestamp).toISOString(), // NEW: Format timestamp using user's timezone
timestamp: formatInTimeZone(new Date(translatedPayload.timestamp), userTimezone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), // Example format, adjust as needed
// Use the translated message if available, otherwise stringify // Use the translated message if available, otherwise stringify
details: (typeof translatedPayload.details === 'object' && translatedPayload.details?.message) details: (typeof translatedPayload.details === 'object' && translatedPayload.details?.message)
? translatedPayload.details.message ? translatedPayload.details.message
@@ -394,7 +399,7 @@ export class NotificationService {
} }
} }
private async _sendEmail(setting: NotificationSetting, payload: NotificationPayload, userLang: string): Promise<void> { private async _sendEmail(setting: NotificationSetting, payload: NotificationPayload, userLang: string, userTimezone: string): Promise<void> { // Add userTimezone
const config = setting.config as EmailConfig; const config = setting.config as EmailConfig;
if (!config.to || !config.smtpHost || !config.smtpPort || !config.from) { if (!config.to || !config.smtpHost || !config.smtpPort || !config.from) {
console.error(`[通知] 邮件设置 ID ${setting.id} 缺少必要的 SMTP 配置 (to, smtpHost, smtpPort, from)。`); console.error(`[通知] 邮件设置 ID ${setting.id} 缺少必要的 SMTP 配置 (to, smtpHost, smtpPort, from)。`);
@@ -432,7 +437,8 @@ export class NotificationService {
const templateDataEmailSubject: Record<string, string> = { const templateDataEmailSubject: Record<string, string> = {
event: payload.event, event: payload.event,
eventDisplay: eventDisplayName, // Assuming subject doesn't need markdown eventDisplay: eventDisplayName, // Assuming subject doesn't need markdown
timestamp: new Date(payload.timestamp).toISOString(), // NEW: Format timestamp using user's timezone
timestamp: formatInTimeZone(new Date(payload.timestamp), userTimezone, "yyyy-MM-dd HH:mm:ss zzz"), // Example format for email
details: typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2), details: typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2),
// Add other relevant fields from i18nOptions if needed by subject template // Add other relevant fields from i18nOptions if needed by subject template
...Object.entries(i18nOptions).reduce((acc, [key, value]) => { ...Object.entries(i18nOptions).reduce((acc, [key, value]) => {
@@ -447,9 +453,11 @@ export class NotificationService {
const bodyKey = `eventBody.${payload.event}`; const bodyKey = `eventBody.${payload.event}`;
const detailsString = typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2); const detailsString = typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2);
const defaultBodyText = `Event: ${eventDisplayName}\nTimestamp: ${new Date(payload.timestamp).toISOString()}\nDetails:\n${detailsString}`; // NEW: Use formatted timestamp in default body text
const body = i18next.t(bodyKey, { ...i18nOptions, defaultValue: defaultBodyText, eventDisplay: eventDisplayName }); const formattedTimestampForEmail = formatInTimeZone(new Date(payload.timestamp), userTimezone, "yyyy-MM-dd HH:mm:ss zzz");
const defaultBodyText = `Event: ${eventDisplayName}\nTimestamp: ${formattedTimestampForEmail}\nDetails:\n${detailsString}`;
// Pass formatted timestamp to i18n interpolation as well
const body = i18next.t(bodyKey, { ...i18nOptions, timestamp: formattedTimestampForEmail, defaultValue: defaultBodyText, eventDisplay: eventDisplayName });
const mailOptions: Mail.Options = { const mailOptions: Mail.Options = {
from: config.from, from: config.from,
to: config.to, to: config.to,
@@ -466,8 +474,8 @@ export class NotificationService {
} }
} }
private async _sendTelegram(setting: NotificationSetting, payload: NotificationPayload, userLang: string): Promise<void> { private async _sendTelegram(setting: NotificationSetting, payload: NotificationPayload, userLang: string, userTimezone: string): Promise<void> { // Add userTimezone
console.log(`[_sendTelegram] Initiating for event: ${payload.event}, Setting ID: ${setting.id}, Lang: ${userLang}`); console.log(`[_sendTelegram] Initiating for event: ${payload.event}, Setting ID: ${setting.id}, Lang: ${userLang}, Timezone: ${userTimezone}`); // Modified log
console.log(`[_sendTelegram] Received payload:`, JSON.stringify(payload, null, 2)); console.log(`[_sendTelegram] Received payload:`, JSON.stringify(payload, null, 2));
const config = setting.config as TelegramConfig; const config = setting.config as TelegramConfig;
if (!config.botToken || !config.chatId) { if (!config.botToken || !config.chatId) {
@@ -496,8 +504,8 @@ export class NotificationService {
const templateData: Record<string, string> = { const templateData: Record<string, string> = {
// Assign the *translated* event name to the 'event' key (NO escaping) // Assign the *translated* event name to the 'event' key (NO escaping)
event: translatedEventName, event: translatedEventName,
// ISO timestamp (usually safe) // NEW: Format timestamp using user's timezone for Telegram (adjust format as needed)
timestamp: new Date(payload.timestamp).toISOString(), timestamp: formatInTimeZone(new Date(payload.timestamp), userTimezone, "yyyy-MM-dd HH:mm:ss zzz"),
// Formatted details string (NO escaping) // Formatted details string (NO escaping)
details: detailsText details: detailsText
// Note: We no longer create eventDisplay key // Note: We no longer create eventDisplay key
@@ -42,7 +42,8 @@ export const settingsController = {
'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++ 'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++
'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++ 'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++
'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++ 'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++
'commandInputSyncTarget' // +++ 添加命令输入同步目标键 +++ 'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++
'timezone' // NEW: 添加时区键
]; ];
const filteredSettings: Record<string, string> = {}; const filteredSettings: Record<string, string> = {};
for (const key in settingsToUpdate) { for (const key in settingsToUpdate) {
+13 -2
View File
@@ -44,6 +44,7 @@ interface SettingsState {
fileManagerRowSizeMultiplier?: string; // NEW: 文件管理器行大小乘数 (e.g., '1.0') fileManagerRowSizeMultiplier?: string; // NEW: 文件管理器行大小乘数 (e.g., '1.0')
fileManagerColWidths?: string; // NEW: 文件管理器列宽 JSON 字符串 (e.g., '{"name": 300, "size": 100}') fileManagerColWidths?: string; // NEW: 文件管理器列宽 JSON 字符串 (e.g., '{"name": 300, "size": 100}')
commandInputSyncTarget?: 'quickCommands' | 'commandHistory' | 'none'; // NEW: 命令输入同步目标 commandInputSyncTarget?: 'quickCommands' | 'commandHistory' | 'none'; // NEW: 命令输入同步目标
timezone?: string; // NEW: 时区设置 (e.g., 'Asia/Shanghai', 'UTC')
// Add other general settings keys here as needed // Add other general settings keys here as needed
[key: string]: string | undefined; // Allow other string settings [key: string]: string | undefined; // Allow other string settings
} }
@@ -206,6 +207,10 @@ export const useSettingsStore = defineStore('settings', () => {
if (settings.value.commandInputSyncTarget === undefined) { if (settings.value.commandInputSyncTarget === undefined) {
settings.value.commandInputSyncTarget = 'none'; // 默认不同步 settings.value.commandInputSyncTarget = 'none'; // 默认不同步
} }
// NEW: Timezone default
if (settings.value.timezone === undefined) {
settings.value.timezone = 'UTC'; // 默认 UTC
}
// --- 语言设置 --- // --- 语言设置 ---
const langFromSettings = settings.value.language; const langFromSettings = settings.value.language;
@@ -282,7 +287,8 @@ export const useSettingsStore = defineStore('settings', () => {
'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++ 'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++
'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++ 'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++
'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++ 'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++
'commandInputSyncTarget' // +++ 添加命令输入同步目标键 +++ 'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++
'timezone' // NEW: 添加时区键
]; ];
if (!allowedKeys.includes(key)) { if (!allowedKeys.includes(key)) {
console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`); console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`);
@@ -323,7 +329,8 @@ export const useSettingsStore = defineStore('settings', () => {
'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++ 'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++
'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++ 'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++
'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++ 'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++
'commandInputSyncTarget' // +++ 添加命令输入同步目标键 +++ 'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++
'timezone' // NEW: 添加时区键
]; ];
const filteredUpdates: Partial<SettingsState> = {}; const filteredUpdates: Partial<SettingsState> = {};
let languageUpdate: string | undefined = undefined; // Use string type let languageUpdate: string | undefined = undefined; // Use string type
@@ -547,6 +554,9 @@ export const useSettingsStore = defineStore('settings', () => {
return 'none'; // Default to 'none' if invalid or not set return 'none'; // Default to 'none' if invalid or not set
}); });
// NEW: Getter for timezone setting
const timezone = computed(() => settings.value.timezone || 'UTC'); // Fallback to UTC
// --- CAPTCHA Getters (Public Only) --- // --- CAPTCHA Getters (Public Only) ---
const isCaptchaEnabled = computed(() => captchaSettings.value?.enabled ?? false); const isCaptchaEnabled = computed(() => captchaSettings.value?.enabled ?? false);
const captchaProvider = computed(() => captchaSettings.value?.provider ?? 'none'); const captchaProvider = computed(() => captchaSettings.value?.provider ?? 'none');
@@ -584,5 +594,6 @@ export const useSettingsStore = defineStore('settings', () => {
updateSidebarPaneWidth, // +++ 暴露更新特定面板宽度的 action +++ updateSidebarPaneWidth, // +++ 暴露更新特定面板宽度的 action +++
updateFileManagerLayoutSettings, // +++ 暴露更新文件管理器布局的 action +++ updateFileManagerLayoutSettings, // +++ 暴露更新文件管理器布局的 action +++
commandInputSyncTarget, // +++ 暴露命令输入同步目标 getter +++ commandInputSyncTarget, // +++ 暴露命令输入同步目标 getter +++
timezone, // NEW: 暴露时区 getter
}; };
}); });
+59 -1
View File
@@ -483,6 +483,31 @@
</div> </div>
</form> </form>
</div> </div>
<hr class="border-border/50"> <!-- NEW: Separator -->
<!-- Timezone Setting -->
<div class="settings-section-content">
<h3 class="text-base font-semibold text-foreground mb-3">{{ t('settings.timezone.title', '时区设置') }}</h3>
<form @submit.prevent="handleUpdateTimezone" class="space-y-4">
<div>
<label for="timezoneSelect" class="block text-sm font-medium text-text-secondary mb-1">{{ t('settings.timezone.selectLabel', '选择时区') }}</label>
<select id="timezoneSelect" v-model="selectedTimezone"
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary appearance-none bg-no-repeat bg-right pr-8"
style="background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\'%3e%3cpath fill=\'none\' stroke=\'%236c757d\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M2 5l6 6 6-6\'/%3e%3c/svg%3e'); background-position: right 0.75rem center; background-size: 16px 12px;">
<option v-for="tz in commonTimezones" :key="tz" :value="tz">
{{ tz }}
</option>
</select>
<small class="block mt-1 text-xs text-text-secondary">{{ t('settings.timezone.description', '通知中的时间戳将根据此时区进行格式化。') }}</small>
</div>
<div class="flex items-center justify-between">
<button type="submit" :disabled="timezoneLoading"
class="px-4 py-2 bg-button text-button-text rounded-md shadow-sm hover:bg-button-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out text-sm font-medium">
{{ timezoneLoading ? $t('common.saving') : t('common.save') }}
</button>
<p v-if="timezoneMessage" :class="['text-sm', timezoneSuccess ? 'text-success' : 'text-error']">{{ timezoneMessage }}</p>
</div>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -591,7 +616,10 @@ const workspaceSidebarPersistentSuccess = ref(false); // 新增
const commandInputSyncLoading = ref(false); // NEW const commandInputSyncLoading = ref(false); // NEW
const commandInputSyncMessage = ref(''); // NEW const commandInputSyncMessage = ref(''); // NEW
const commandInputSyncSuccess = ref(false); // NEW const commandInputSyncSuccess = ref(false); // NEW
const selectedTimezone = ref('UTC'); // v-model
const timezoneLoading = ref(false);
const timezoneMessage = ref('');
const timezoneSuccess = ref(false);
// CAPTCHA Form State // CAPTCHA Form State
const captchaForm = reactive<UpdateCaptchaSettingsDto>({ // Use reactive for the form object const captchaForm = reactive<UpdateCaptchaSettingsDto>({ // Use reactive for the form object
enabled: false, enabled: false,
@@ -605,6 +633,17 @@ const captchaLoading = ref(false);
const captchaMessage = ref(''); const captchaMessage = ref('');
const captchaSuccess = ref(false); const captchaSuccess = ref(false);
//
const commonTimezones = ref([
'UTC',
'Etc/GMT+12', 'Pacific/Midway', 'Pacific/Honolulu', 'America/Anchorage',
'America/Los_Angeles', 'America/Denver', 'America/Chicago', 'America/New_York',
'America/Caracas', 'America/Halifax', 'America/Sao_Paulo', 'Atlantic/Azores',
'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Moscow',
'Asia/Dubai', 'Asia/Karachi', 'Asia/Dhaka', 'Asia/Bangkok',
'Asia/Shanghai', 'Asia/Tokyo', 'Australia/Sydney', 'Pacific/Auckland',
'Etc/GMT-14'
]);
// --- Watcher to sync local form state with store state --- // --- Watcher to sync local form state with store state ---
watch(settings, (newSettings, oldSettings) => { watch(settings, (newSettings, oldSettings) => {
@@ -625,6 +664,7 @@ watch(settings, (newSettings, oldSettings) => {
statusMonitorIntervalLocal.value = statusMonitorIntervalSecondsNumber.value; // statusMonitorIntervalLocal.value = statusMonitorIntervalSecondsNumber.value; //
workspaceSidebarPersistentEnabled.value = workspaceSidebarPersistentBoolean.value; // workspaceSidebarPersistentEnabled.value = workspaceSidebarPersistentBoolean.value; //
commandInputSyncTargetLocal.value = commandInputSyncTarget.value; // NEW: Sync command input sync target commandInputSyncTargetLocal.value = commandInputSyncTarget.value; // NEW: Sync command input sync target
selectedTimezone.value = newSettings.timezone || 'UTC'; //
}, { deep: true, immediate: true }); // immediate: true to run on initial load }, { deep: true, immediate: true }); // immediate: true to run on initial load
@@ -795,6 +835,24 @@ const handleUpdateCommandInputSyncTarget = async () => {
} }
}; };
// --- Timezone setting method ---
const handleUpdateTimezone = async () => {
timezoneLoading.value = true;
timezoneMessage.value = '';
timezoneSuccess.value = false;
try {
await settingsStore.updateSetting('timezone', selectedTimezone.value);
timezoneMessage.value = t('settings.timezone.success.saved', '时区设置已保存'); //
timezoneSuccess.value = true;
} catch (error: any) {
console.error('更新时区设置失败:', error);
timezoneMessage.value = error.message || t('settings.timezone.error.saveFailed', '保存时区设置失败'); //
timezoneSuccess.value = false;
} finally {
timezoneLoading.value = false;
}
};
// --- --- // --- ---
const openStyleCustomizer = () => { const openStyleCustomizer = () => {
appearanceStore.toggleStyleCustomizer(true); appearanceStore.toggleStyleCustomizer(true);