feat(webui): add Tailwind build system and refactor frontend architecture
- Replace Tailwind CDN with local build (PostCSS + autoprefixer + daisyui) - Add CSS build scripts with automatic prepare hook on npm install - Create account-actions.js service layer with unified response format - Extend ErrorHandler.withLoading() for automatic loading state management - Add skeleton screens for initial load, silent refresh for subsequent updates - Implement loading animations for async operations (buttons, modals) - Improve empty states and add ARIA labels for accessibility - Abstract component styles using @apply (buttons, badges, inputs) - Add JSDoc documentation for Dashboard modules - Update README and CLAUDE.md with development guidelines
This commit is contained in:
199
public/js/utils/account-actions.js
Normal file
199
public/js/utils/account-actions.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Account Actions Service
|
||||
* 纯业务逻辑层 - 处理账号操作的 HTTP 请求、乐观更新和数据刷新
|
||||
* 不包含 UI 关注点(Toast、Loading、模态框由组件层处理)
|
||||
*/
|
||||
window.AccountActions = window.AccountActions || {};
|
||||
|
||||
/**
|
||||
* 刷新账号 token 和配额信息
|
||||
* @param {string} email - 账号邮箱
|
||||
* @returns {Promise<{success: boolean, data?: object, error?: string}>}
|
||||
*/
|
||||
window.AccountActions.refreshAccount = async function(email) {
|
||||
const store = Alpine.store('global');
|
||||
|
||||
try {
|
||||
const { response, newPassword } = await window.utils.request(
|
||||
`/api/accounts/${encodeURIComponent(email)}/refresh`,
|
||||
{ method: 'POST' },
|
||||
store.webuiPassword
|
||||
);
|
||||
|
||||
if (newPassword) {
|
||||
store.webuiPassword = newPassword;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status !== 'ok') {
|
||||
return { success: false, error: data.error || 'Refresh failed' };
|
||||
}
|
||||
|
||||
// 触发数据刷新
|
||||
await Alpine.store('data').fetchData();
|
||||
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换账号启用/禁用状态(包含乐观更新和错误回滚)
|
||||
* @param {string} email - 账号邮箱
|
||||
* @param {boolean} enabled - 目标状态(true=启用, false=禁用)
|
||||
* @returns {Promise<{success: boolean, rolledBack?: boolean, data?: object, error?: string}>}
|
||||
*/
|
||||
window.AccountActions.toggleAccount = async function(email, enabled) {
|
||||
const store = Alpine.store('global');
|
||||
const dataStore = Alpine.store('data');
|
||||
|
||||
// 乐观更新:立即修改 UI
|
||||
const account = dataStore.accounts.find(a => a.email === email);
|
||||
const previousState = account ? account.enabled : !enabled;
|
||||
|
||||
if (account) {
|
||||
account.enabled = enabled;
|
||||
}
|
||||
|
||||
try {
|
||||
const { response, newPassword } = await window.utils.request(
|
||||
`/api/accounts/${encodeURIComponent(email)}/toggle`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled })
|
||||
},
|
||||
store.webuiPassword
|
||||
);
|
||||
|
||||
if (newPassword) {
|
||||
store.webuiPassword = newPassword;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status !== 'ok') {
|
||||
throw new Error(data.error || 'Toggle failed');
|
||||
}
|
||||
|
||||
// 确认服务器状态
|
||||
await dataStore.fetchData();
|
||||
return { success: true, data };
|
||||
|
||||
} catch (error) {
|
||||
// 错误回滚:恢复原状态
|
||||
if (account) {
|
||||
account.enabled = previousState;
|
||||
}
|
||||
await dataStore.fetchData();
|
||||
return { success: false, error: error.message, rolledBack: true };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除账号
|
||||
* @param {string} email - 账号邮箱
|
||||
* @returns {Promise<{success: boolean, data?: object, error?: string}>}
|
||||
*/
|
||||
window.AccountActions.deleteAccount = async function(email) {
|
||||
const store = Alpine.store('global');
|
||||
|
||||
try {
|
||||
const { response, newPassword } = await window.utils.request(
|
||||
`/api/accounts/${encodeURIComponent(email)}`,
|
||||
{ method: 'DELETE' },
|
||||
store.webuiPassword
|
||||
);
|
||||
|
||||
if (newPassword) {
|
||||
store.webuiPassword = newPassword;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status !== 'ok') {
|
||||
return { success: false, error: data.error || 'Delete failed' };
|
||||
}
|
||||
|
||||
// 触发数据刷新
|
||||
await Alpine.store('data').fetchData();
|
||||
return { success: true, data };
|
||||
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取账号重新认证的 OAuth URL
|
||||
* 注意:此方法仅返回 URL,不打开窗口(由组件层决定如何处理)
|
||||
* @param {string} email - 账号邮箱
|
||||
* @returns {Promise<{success: boolean, url?: string, error?: string}>}
|
||||
*/
|
||||
window.AccountActions.getFixAccountUrl = async function(email) {
|
||||
const store = Alpine.store('global');
|
||||
|
||||
try {
|
||||
const urlPath = `/api/auth/url?email=${encodeURIComponent(email)}`;
|
||||
const { response, newPassword } = await window.utils.request(
|
||||
urlPath,
|
||||
{},
|
||||
store.webuiPassword
|
||||
);
|
||||
|
||||
if (newPassword) {
|
||||
store.webuiPassword = newPassword;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status !== 'ok') {
|
||||
return { success: false, error: data.error || 'Failed to get auth URL' };
|
||||
}
|
||||
|
||||
return { success: true, url: data.url };
|
||||
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从磁盘重新加载所有账号配置
|
||||
* @returns {Promise<{success: boolean, data?: object, error?: string}>}
|
||||
*/
|
||||
window.AccountActions.reloadAccounts = async function() {
|
||||
const store = Alpine.store('global');
|
||||
|
||||
try {
|
||||
const { response, newPassword } = await window.utils.request(
|
||||
'/api/accounts/reload',
|
||||
{ method: 'POST' },
|
||||
store.webuiPassword
|
||||
);
|
||||
|
||||
if (newPassword) {
|
||||
store.webuiPassword = newPassword;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status !== 'ok') {
|
||||
return { success: false, error: data.error || 'Reload failed' };
|
||||
}
|
||||
|
||||
// 触发数据刷新
|
||||
await Alpine.store('data').fetchData();
|
||||
return { success: true, data };
|
||||
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查账号是否可以删除
|
||||
* 来自 Antigravity 数据库的账号(source='database')不可删除
|
||||
* @param {object} account - 账号对象
|
||||
* @returns {boolean} true 表示可删除
|
||||
*/
|
||||
window.AccountActions.canDelete = function(account) {
|
||||
return account && account.source !== 'database';
|
||||
};
|
||||
@@ -105,3 +105,41 @@ window.ErrorHandler.apiCall = async function(apiCall, successMessage = null, err
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute an async function with automatic loading state management
|
||||
* @param {Function} asyncFn - Async function to execute
|
||||
* @param {object} context - Component context (this) that contains the loading state
|
||||
* @param {string} loadingKey - Name of the loading state property (default: 'loading')
|
||||
* @param {object} options - Additional options (same as safeAsync)
|
||||
* @returns {Promise<any>} Result of the function or undefined on error
|
||||
*
|
||||
* @example
|
||||
* // In your Alpine component:
|
||||
* async refreshAccount(email) {
|
||||
* return await window.ErrorHandler.withLoading(async () => {
|
||||
* const response = await window.utils.request(`/api/accounts/${email}/refresh`, { method: 'POST' });
|
||||
* this.$store.global.showToast('Account refreshed', 'success');
|
||||
* return response;
|
||||
* }, this, 'refreshing');
|
||||
* }
|
||||
*
|
||||
* // In HTML:
|
||||
* // <button @click="refreshAccount(email)" :disabled="refreshing">
|
||||
* // <i class="fas fa-sync-alt" :class="{ 'fa-spin': refreshing }"></i>
|
||||
* // Refresh
|
||||
* // </button>
|
||||
*/
|
||||
window.ErrorHandler.withLoading = async function(asyncFn, context, loadingKey = 'loading', options = {}) {
|
||||
// Set loading state to true
|
||||
context[loadingKey] = true;
|
||||
|
||||
try {
|
||||
// Execute the async function with error handling
|
||||
const result = await window.ErrorHandler.safeAsync(asyncFn, options.errorMessage, options);
|
||||
return result;
|
||||
} finally {
|
||||
// Always reset loading state, even if there was an error
|
||||
context[loadingKey] = false;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user