init: 整合项目(dashboard + www + docs)

This commit is contained in:
eafonyang
2026-07-30 11:23:50 +08:00
commit 99ab6cc4f4
435 changed files with 61037 additions and 0 deletions
@@ -0,0 +1,40 @@
import { request } from '../request';
/**
* Login
*
* @param userName User name
* @param password Password
*/
export function fetchLogin(userName: string, password: string) {
return request<Api.Auth.LoginResult>({
url: '/auth/login',
method: 'post',
data: {
username: userName,
password
}
});
}
/** Get current user info */
export function fetchGetUserInfo() {
return request<Api.Auth.DashboardUser>({ url: '/auth/me' });
}
/**
* Change password
*
* @param oldPassword Old password
* @param newPassword New password
*/
export function fetchChangePassword(oldPassword: string, newPassword: string) {
return request({
url: '/auth/password',
method: 'put',
data: {
old_password: oldPassword,
new_password: newPassword
}
});
}
@@ -0,0 +1,39 @@
import { request } from '../request';
import type { PageParams } from './brand';
export function fetchGetBlackList(
params?: PageParams & {
ota_id?: number;
model?: string;
mac?: string;
}
) {
return request<Api.Common.PageResult<Api.Blacklist.Item>>({
url: '/blacklist/',
method: 'get',
params
});
}
export function fetchCreateBlackList(data: { ota_id: number; mac: string }) {
return request<Api.Blacklist.Item>({
url: '/blacklist/',
method: 'post',
data
});
}
export function fetchUpdateBlackList(id: number, data: Record<string, unknown>) {
return request<Api.Blacklist.Item>({
url: `/blacklist/${id}`,
method: 'put',
data
});
}
export function fetchDeleteBlackList(id: number) {
return request({
url: `/blacklist/${id}`,
method: 'delete'
});
}
@@ -0,0 +1,45 @@
import { request } from '../request';
/** Page list params used by dashboard list APIs */
export interface PageParams {
skip?: number;
limit?: number;
}
export function fetchGetBrands(params?: PageParams & { name?: string }) {
return request<Api.Common.PageResult<Api.Brand.Brand>>({
url: '/brands/',
method: 'get',
params
});
}
export function fetchGetBrand(id: number) {
return request<Api.Brand.Brand>({
url: `/brands/${id}`,
method: 'get'
});
}
export function fetchCreateBrand(data: Api.Brand.BrandPayload) {
return request<Api.Brand.Brand>({
url: '/brands/',
method: 'post',
data
});
}
export function fetchUpdateBrand(data: Api.Brand.BrandPayload & { id: number }) {
return request<Api.Brand.Brand>({
url: `/brands/${data.id}`,
method: 'put',
data
});
}
export function fetchDeleteBrand(id: number) {
return request({
url: `/brands/${id}`,
method: 'delete'
});
}
@@ -0,0 +1,24 @@
import { request } from '../request';
export function fetchGetDashboardToday() {
return request<Api.Dashboard.TodayStats>({
url: '/dashboard/today',
method: 'get'
});
}
export function fetchGetDeviceDaily(days: number = 30) {
return request<Api.Dashboard.DeviceDailyStats>({
url: '/dashboard/device-daily',
method: 'get',
params: { days }
});
}
export function fetchGetActiveDaily(days: number = 30) {
return request<Api.Dashboard.ActiveDailyStats>({
url: '/dashboard/active-daily',
method: 'get',
params: { days }
});
}
@@ -0,0 +1,10 @@
export * from './auth';
export * from './route';
export * from './brand';
export * from './dashboard';
export * from './user';
export * from './ota';
export * from './blacklist';
export * from './ota-target-device';
export * from './share-code-log';
export * from './model';
+131
View File
@@ -0,0 +1,131 @@
import { request } from '../request';
import type { PageParams } from './brand';
export function fetchGetModels(
params?: PageParams & {
brand_name?: string;
name?: string;
sort_by?: string;
sort_order?: string;
}
) {
return request<Api.Common.PageResult<Api.Model.ModelItem>>({
url: '/models/',
method: 'get',
params
});
}
export function fetchGetModel(id: number) {
return request<Api.Model.ModelItem>({
url: `/models/${id}`,
method: 'get'
});
}
export function fetchCreateModel(data: FormData | Record<string, unknown>, isFormData = false) {
return request<Api.Model.ModelItem>({
url: '/models/',
method: 'post',
data,
headers: isFormData ? { 'Content-Type': 'multipart/form-data' } : {}
});
}
export function fetchUpdateModel(id: number, data: FormData | Record<string, unknown>, isFormData = false) {
return request<Api.Model.ModelItem>({
url: `/models/${id}`,
method: 'put',
data,
headers: isFormData ? { 'Content-Type': 'multipart/form-data' } : {}
});
}
export function fetchDeleteModel(id: number) {
return request({
url: `/models/${id}`,
method: 'delete'
});
}
export function fetchGetModelEqCache(id: number) {
return request({
url: `/models/${id}/eq-cache`,
method: 'get',
timeout: 30000
});
}
export function fetchGetModelEqCacheField(id: number, fieldKey: string) {
return request({
url: `/models/${id}/eq-cache/field`,
method: 'get',
params: { key: fieldKey },
timeout: 30000
});
}
export function fetchGetModelMeilisearch(id: number) {
return request({
url: `/models/${id}/meilisearch`,
method: 'get',
timeout: 30000
});
}
export function fetchGetModelMeasurement(id: number) {
return request({
url: `/models/${id}/measurement`,
method: 'get',
timeout: 30000
});
}
export function fetchValidatePushToMeilisearch(modelIds: number[]) {
return request({
url: '/models/push-to-search/validate',
method: 'post',
data: { model_ids: modelIds },
timeout: 120000,
// @ts-expect-error custom flag consumed by request onError
skipErrorToast: true
});
}
export function fetchPushToMeilisearch(modelIds: number[]) {
return request({
url: '/models/push-to-search',
method: 'post',
data: { model_ids: modelIds },
timeout: 60000,
// @ts-expect-error custom flag consumed by request onError
skipErrorToast: true
});
}
export function fetchBatchUpdateModels(data: {
ids: number[];
brand_name?: string;
form?: string;
source?: string;
rig?: string;
}) {
return request<{
updated_count: number;
updated_ids: number[];
errors: Array<{ id: number; brand_name: string; name: string; error: string }>;
}>({
url: '/models/batch-update',
method: 'patch',
data
});
}
export function fetchFromSquigLink(shareUrl: string, selectedFile?: string) {
return request({
url: '/models/squiglink-fetch',
method: 'post',
data: { share_url: shareUrl, selected_file: selectedFile },
timeout: 30000
});
}
@@ -0,0 +1,39 @@
import { request } from '../request';
import type { PageParams } from './brand';
export function fetchGetOtaTargetDeviceList(
params?: PageParams & {
ota_id?: number;
model?: string;
mac_addr?: string;
}
) {
return request<Api.Common.PageResult<Api.OtaTargetDevice.Item>>({
url: '/ota-target-device/',
method: 'get',
params
});
}
export function fetchCreateOtaTargetDevice(data: { ota_id: number; mac_addr: string }) {
return request<Api.OtaTargetDevice.Item>({
url: '/ota-target-device/',
method: 'post',
data
});
}
export function fetchUpdateOtaTargetDevice(id: number, data: Record<string, unknown>) {
return request<Api.OtaTargetDevice.Item>({
url: `/ota-target-device/${id}`,
method: 'put',
data
});
}
export function fetchDeleteOtaTargetDevice(id: number) {
return request({
url: `/ota-target-device/${id}`,
method: 'delete'
});
}
+61
View File
@@ -0,0 +1,61 @@
import { request } from '../request';
import type { PageParams } from './brand';
export function fetchGetOtaList(
params?: PageParams & {
verCode?: number;
verName?: string;
model?: string;
status?: number;
beta?: number;
}
) {
return request<Api.Common.PageResult<Api.Ota.OtaItem>>({
url: '/ota/',
method: 'get',
params
});
}
export function fetchGetOta(id: number) {
return request<Api.Ota.OtaItem>({
url: `/ota/${id}`,
method: 'get'
});
}
export function fetchCreateOta(data: Record<string, unknown>) {
return request<Api.Ota.OtaItem>({
url: '/ota/',
method: 'post',
data
});
}
export function fetchUpdateOta(id: number, data: Record<string, unknown>) {
return request<Api.Ota.OtaItem>({
url: `/ota/${id}`,
method: 'put',
data
});
}
export function fetchDeleteOta(id: number) {
return request({
url: `/ota/${id}`,
method: 'delete'
});
}
export function fetchUploadOtaPackage(file: File, model: string) {
const formData = new FormData();
formData.append('package_file', file);
formData.append('model', model);
return request({
url: '/ota/upload-package',
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 300000
});
}
@@ -0,0 +1,20 @@
import { request } from '../request';
/** get constant routes */
export function fetchGetConstantRoutes() {
return request<Api.Route.MenuRoute[]>({ url: '/route/getConstantRoutes' });
}
/** get user routes */
export function fetchGetUserRoutes() {
return request<Api.Route.UserRoute>({ url: '/route/getUserRoutes' });
}
/**
* whether the route is exist
*
* @param routeName route name
*/
export function fetchIsRouteExist(routeName: string) {
return request<boolean>({ url: '/route/isRouteExist', params: { routeName } });
}
@@ -0,0 +1,22 @@
import { request } from '../request';
import type { PageParams } from './brand';
export function fetchGetShareCodeLogs(
params?: PageParams & {
mac_addr?: string;
share_code?: string;
action?: 'export' | 'import';
ip_addr?: string;
start_at?: string;
end_at?: string;
sort_by?: 'id' | 'create_at';
sort_order?: 'asc' | 'desc';
}
) {
return request<Api.Common.PageResult<Api.ShareCodeLog.Item>>({
url: '/share-code/logs',
method: 'get',
params,
timeout: 30000
});
}
@@ -0,0 +1,33 @@
import { request } from '../request';
import type { PageParams } from './brand';
export function fetchGetUsers(params?: PageParams & { username?: string }) {
return request<Api.Common.PageResult<Api.User.User>>({
url: '/users/',
method: 'get',
params
});
}
export function fetchCreateUser(data: Api.User.CreatePayload) {
return request<Api.User.User>({
url: '/users/',
method: 'post',
data
});
}
export function fetchUpdateUser(id: number, data: Api.User.UpdatePayload) {
return request<Api.User.User>({
url: `/users/${id}`,
method: 'put',
data
});
}
export function fetchDeleteUser(id: number) {
return request({
url: `/users/${id}`,
method: 'delete'
});
}
@@ -0,0 +1,181 @@
import type { AxiosResponse } from 'axios';
import { BACKEND_ERROR_CODE, createFlatRequest, createRequest } from '@sa/axios';
import { useAuthStore } from '@/store/modules/auth';
import { localStg } from '@/utils/storage';
import { getServiceBaseURL } from '@/utils/service';
import { $t } from '@/locales';
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared';
import type { RequestInstanceState } from './type';
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
/** Dashboard backend treats code 1 as success and code 2 as "no data" (still ok). */
function isDashboardSuccessCode(code: unknown) {
const value = String(code);
const successCode = String(import.meta.env.VITE_SERVICE_SUCCESS_CODE || '1');
return value === successCode || value === '2';
}
export const request = createFlatRequest(
{
baseURL,
timeout: 30000
},
{
defaultState: {
errMsgStack: [],
refreshTokenPromise: null
} as RequestInstanceState,
transform(response: AxiosResponse<App.Service.Response<any>>) {
return response.data.data;
},
async onRequest(config) {
const Authorization = getAuthorization();
Object.assign(config.headers, { Authorization });
return config;
},
isBackendSuccess(response) {
return isDashboardSuccessCode(response.data.code);
},
async onBackendFail(response, instance) {
const authStore = useAuthStore();
const responseCode = String(response.data.code);
function handleLogout() {
authStore.resetStore();
}
function logoutAndCleanup() {
handleLogout();
window.removeEventListener('beforeunload', handleLogout);
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg);
}
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',').filter(Boolean) || [];
if (logoutCodes.includes(responseCode)) {
handleLogout();
return null;
}
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',').filter(Boolean) || [];
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) {
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
window.addEventListener('beforeunload', handleLogout);
window.$dialog?.error({
title: $t('common.error'),
content: response.data.msg,
positiveText: $t('common.confirm'),
maskClosable: false,
closeOnEsc: false,
onPositiveClick() {
logoutAndCleanup();
},
onClose() {
logoutAndCleanup();
}
});
return null;
}
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',').filter(Boolean) || [];
if (expiredTokenCodes.includes(responseCode)) {
const success = await handleExpiredRequest(request.state);
if (success) {
const Authorization = getAuthorization();
Object.assign(response.config.headers, { Authorization });
return instance.request(response.config) as Promise<AxiosResponse>;
}
}
return null;
},
onError(error) {
const status = error.response?.status;
// HTTP 401 from dashboard auth middleware
if (status === 401) {
const authStore = useAuthStore();
authStore.resetStore();
const detail = (error.response?.data as { detail?: string } | undefined)?.detail;
if (detail) {
showErrorMsg(request.state, typeof detail === 'string' ? detail : $t('request.logoutMsg'));
}
return;
}
if (status === 403) {
const detail = (error.response?.data as { detail?: string } | undefined)?.detail;
showErrorMsg(request.state, typeof detail === 'string' ? detail : '无权限执行此操作');
return;
}
let message = error.message;
let backendErrorCode = '';
if (error.code === BACKEND_ERROR_CODE) {
message = error.response?.data?.msg || message;
backendErrorCode = String(error.response?.data?.code || '');
}
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',').filter(Boolean) || [];
if (modalLogoutCodes.includes(backendErrorCode)) {
return;
}
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',').filter(Boolean) || [];
if (expiredTokenCodes.includes(backendErrorCode)) {
return;
}
// Allow callers to suppress toast (e.g. model push progress dialog)
const cfg = error.config as (typeof error.config & { skipErrorToast?: boolean }) | undefined;
if (cfg?.skipErrorToast) {
return;
}
showErrorMsg(request.state, message);
}
}
);
export const demoRequest = createRequest(
{
baseURL: otherBaseURL.demo
},
{
transform(response: AxiosResponse<App.Service.DemoResponse>) {
return response.data.result;
},
async onRequest(config) {
const { headers } = config;
const token = localStg.get('token');
const Authorization = token ? `Bearer ${token}` : null;
Object.assign(headers, { Authorization });
return config;
},
isBackendSuccess(response) {
return response.data.status === '200';
},
async onBackendFail(_response) {
// noop
},
onError(error) {
let message = error.message;
if (error.code === BACKEND_ERROR_CODE) {
message = error.response?.data?.message || message;
}
window.$message?.error(message);
}
}
);
@@ -0,0 +1,53 @@
import { useAuthStore } from '@/store/modules/auth';
import { localStg } from '@/utils/storage';
import type { RequestInstanceState } from './type';
export function getAuthorization() {
const token = localStg.get('token');
const Authorization = token ? `Bearer ${token}` : null;
return Authorization;
}
/** Dashboard backend has no refresh-token API; always fail and logout. */
async function handleRefreshToken() {
const { resetStore } = useAuthStore();
resetStore();
return false;
}
export async function handleExpiredRequest(state: RequestInstanceState) {
if (!state.refreshTokenPromise) {
state.refreshTokenPromise = handleRefreshToken();
}
const success = await state.refreshTokenPromise;
setTimeout(() => {
state.refreshTokenPromise = null;
}, 1000);
return success;
}
export function showErrorMsg(state: RequestInstanceState, message: string) {
if (!state.errMsgStack?.length) {
state.errMsgStack = [];
}
const isExist = state.errMsgStack.includes(message);
if (!isExist) {
state.errMsgStack.push(message);
window.$message?.error(message, {
onLeave: () => {
state.errMsgStack = state.errMsgStack.filter(msg => msg !== message);
setTimeout(() => {
state.errMsgStack = [];
}, 5000);
}
});
}
}
@@ -0,0 +1,7 @@
export interface RequestInstanceState {
/** the promise of refreshing token */
refreshTokenPromise: Promise<boolean> | null;
/** the request error message stack */
errMsgStack: string[];
[key: string]: unknown;
}