重构项目,方便开发
This commit is contained in:
96
frontend-vue/src/shared/api/admin.ts
Normal file
96
frontend-vue/src/shared/api/admin.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { requestDeleteJson, requestGetJson, requestPostJson, requestPutJson } from '@/shared/api/http'
|
||||
|
||||
export interface AdminUserItem {
|
||||
id: number
|
||||
username?: string
|
||||
role?: 'super_admin' | 'admin' | 'normal' | string
|
||||
creator_username?: string
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
export interface AdminSimpleUser {
|
||||
id: number
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface AdminUsersResponse {
|
||||
success: boolean
|
||||
current_user_role?: 'super_admin' | 'admin' | 'normal' | string
|
||||
admins?: AdminSimpleUser[]
|
||||
items?: AdminUserItem[]
|
||||
total?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface AdminMutationResponse {
|
||||
success: boolean
|
||||
msg?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface AdminHistoryItem {
|
||||
id: number
|
||||
username?: string
|
||||
panel_type?: string
|
||||
created_at?: string
|
||||
long_image_url?: string | null
|
||||
result_urls?: string[]
|
||||
}
|
||||
|
||||
export interface AdminHistoryResponse {
|
||||
success: boolean
|
||||
items?: AdminHistoryItem[]
|
||||
total?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface AdminColumnPermissionItem {
|
||||
column_key?: string
|
||||
}
|
||||
|
||||
export interface AdminColumnPermissionResponse {
|
||||
success: boolean
|
||||
items?: AdminColumnPermissionItem[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function getAdminUsers(query = '') {
|
||||
const suffix = query ? `?${query}` : ''
|
||||
return requestGetJson<AdminUsersResponse>(`/api/admin/users${suffix}`)
|
||||
}
|
||||
|
||||
export function createAdminUser(payload: {
|
||||
username: string
|
||||
password: string
|
||||
role: string
|
||||
created_by_id?: number
|
||||
}) {
|
||||
return requestPostJson<AdminMutationResponse>('/api/admin/user', payload)
|
||||
}
|
||||
|
||||
export function updateAdminUser(
|
||||
userId: number | string,
|
||||
payload: {
|
||||
password?: string
|
||||
role?: string
|
||||
},
|
||||
) {
|
||||
return requestPutJson<AdminMutationResponse>(`/api/admin/user/${userId}`, payload)
|
||||
}
|
||||
|
||||
export function deleteAdminUser(userId: number | string) {
|
||||
return requestDeleteJson<AdminMutationResponse>(`/api/admin/user/${userId}`)
|
||||
}
|
||||
|
||||
export function getAdminHistory(query = '') {
|
||||
const suffix = query ? `?${query}` : ''
|
||||
return requestGetJson<AdminHistoryResponse>(`/api/admin/history${suffix}`)
|
||||
}
|
||||
|
||||
export function getUserColumnPermissions(userId: string | number) {
|
||||
return requestGetJson<AdminColumnPermissionResponse>(`/api/admin/user/${userId}/column-permissions`)
|
||||
}
|
||||
38
frontend-vue/src/shared/api/auth.ts
Normal file
38
frontend-vue/src/shared/api/auth.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { requestGetJson, requestPostJson } from '@/shared/api/http'
|
||||
|
||||
export interface LoginResponse {
|
||||
success: boolean
|
||||
redirect?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface AuthCheckResponse {
|
||||
logged_in: boolean
|
||||
redirect?: string
|
||||
}
|
||||
|
||||
export async function checkAuth() {
|
||||
return requestGetJson<AuthCheckResponse>('/api/auth/check')
|
||||
}
|
||||
|
||||
export async function login(username: string, password: string) {
|
||||
const formData = new FormData()
|
||||
formData.set('username', username)
|
||||
formData.set('password', password)
|
||||
|
||||
return requestPostJson<LoginResponse>('/login', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
try {
|
||||
await requestGetJson('/logout', {
|
||||
validateStatus: () => true,
|
||||
})
|
||||
} catch {
|
||||
// logout should still continue on the client even if backend responds abnormally
|
||||
}
|
||||
}
|
||||
88
frontend-vue/src/shared/api/brand.ts
Normal file
88
frontend-vue/src/shared/api/brand.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { requestDeleteJson, requestGetJson, requestPostJson } from '@/shared/api/http'
|
||||
|
||||
export interface BrandExpandFolderResponse {
|
||||
success: boolean
|
||||
paths?: string[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface BrandTaskResultPaths {
|
||||
zip_url?: string
|
||||
}
|
||||
|
||||
export interface BrandTaskItem {
|
||||
id: number | string
|
||||
status?: 'pending' | 'running' | 'success' | 'failed' | 'cancelled' | string
|
||||
desc?: string
|
||||
file_paths?: string[]
|
||||
created_at?: string
|
||||
progress_total?: number
|
||||
progress_current?: number
|
||||
result_paths?: BrandTaskResultPaths
|
||||
}
|
||||
|
||||
export interface BrandTaskListResponse {
|
||||
success: boolean
|
||||
items?: BrandTaskItem[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface BrandTaskDetailResponse {
|
||||
success: boolean
|
||||
task?: BrandTaskItem
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface BrandTaskMutationResponse {
|
||||
success: boolean
|
||||
task_id?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function getBrandTaskEventsUrl(taskId: string | number) {
|
||||
return `/api/brand/tasks/${taskId}/events`
|
||||
}
|
||||
|
||||
export function getBrandTaskDownloadUrl(taskId: string | number) {
|
||||
return `/api/brand/download/${taskId}`
|
||||
}
|
||||
|
||||
export function getBrandTemplateXlsxUrl() {
|
||||
return '/static/品牌文档格式_模板.xlsx'
|
||||
}
|
||||
|
||||
export function getBrandTemplateZipUrl() {
|
||||
return '/static/模板2-以文件夹方式上传.zip'
|
||||
}
|
||||
|
||||
export function expandBrandFolder(folder: string) {
|
||||
return requestPostJson<BrandExpandFolderResponse>('/api/brand/expand-folder', { folder })
|
||||
}
|
||||
|
||||
export function runBrandNow(paths: string[], strategy: string) {
|
||||
return requestPostJson<BrandTaskMutationResponse>('/api/brand/run', { paths, strategy })
|
||||
}
|
||||
|
||||
export function createBrandTask(paths: string[], strategy: string) {
|
||||
return requestPostJson<BrandTaskMutationResponse>('/api/brand/tasks', { paths, strategy })
|
||||
}
|
||||
|
||||
export function getBrandTasks() {
|
||||
return requestGetJson<BrandTaskListResponse>('/api/brand/tasks')
|
||||
}
|
||||
|
||||
export function getBrandTask(taskId: string | number) {
|
||||
return requestGetJson<BrandTaskDetailResponse>(`/api/brand/tasks/${taskId}`)
|
||||
}
|
||||
|
||||
export function cancelBrandTask(taskId: string | number) {
|
||||
return requestPostJson<BrandTaskMutationResponse>(`/api/brand/tasks/${taskId}/cancel`)
|
||||
}
|
||||
|
||||
export function deleteBrandTask(taskId: string | number) {
|
||||
return requestDeleteJson<BrandTaskMutationResponse>(`/api/brand/tasks/${taskId}`)
|
||||
}
|
||||
|
||||
export function createBrandTaskEvents(taskId: string | number) {
|
||||
return new EventSource(getBrandTaskEventsUrl(taskId))
|
||||
}
|
||||
107
frontend-vue/src/shared/api/http.ts
Normal file
107
frontend-vue/src/shared/api/http.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import axios, {
|
||||
AxiosError,
|
||||
type AxiosInstance,
|
||||
type AxiosRequestConfig,
|
||||
type AxiosResponse,
|
||||
type InternalAxiosRequestConfig,
|
||||
} from 'axios'
|
||||
|
||||
export interface ApiSuccess<T> {
|
||||
success: true
|
||||
msg?: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
export interface ApiFailure {
|
||||
success: false
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type ApiResponse<T> = ApiSuccess<T> | ApiFailure
|
||||
export type RequestOptions<D = unknown> = AxiosRequestConfig<D>
|
||||
|
||||
function extractErrorMessage(error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
const data = error.response?.data
|
||||
|
||||
if (data && typeof data === 'object' && 'error' in data) {
|
||||
return String(data.error)
|
||||
}
|
||||
|
||||
if (typeof data === 'string' && data.trim()) {
|
||||
return data
|
||||
}
|
||||
|
||||
return error.message || '请求失败'
|
||||
}
|
||||
|
||||
return error instanceof Error ? error.message : '请求失败'
|
||||
}
|
||||
|
||||
function createHttpClient(): AxiosInstance {
|
||||
const instance = axios.create({
|
||||
withCredentials: true,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
})
|
||||
|
||||
instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
return config
|
||||
})
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(response: AxiosResponse) => response,
|
||||
(error: unknown) => Promise.reject(new Error(extractErrorMessage(error))),
|
||||
)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
export const http = createHttpClient()
|
||||
|
||||
export async function request<T = unknown, D = unknown>(config: RequestOptions<D>): Promise<T> {
|
||||
const response = await http.request<T, AxiosResponse<T>, D>(config)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export function get<T = unknown>(url: string, config?: RequestOptions) {
|
||||
return request<T>({
|
||||
url,
|
||||
method: 'GET',
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
export function post<T = unknown, D = unknown>(url: string, data?: D, config?: RequestOptions<D>) {
|
||||
return request<T, D>({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
export function put<T = unknown, D = unknown>(url: string, data?: D, config?: RequestOptions<D>) {
|
||||
return request<T, D>({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
export function del<T = unknown>(url: string, config?: RequestOptions) {
|
||||
return request<T>({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
export const requestJson = request
|
||||
export const requestGetJson = get
|
||||
export const requestPostJson = post
|
||||
export const requestPutJson = put
|
||||
export const requestDeleteJson = del
|
||||
22
frontend-vue/src/shared/api/update.ts
Normal file
22
frontend-vue/src/shared/api/update.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { requestGetJson, requestPostJson } from '@/shared/api/http'
|
||||
|
||||
export interface VersionResponse {
|
||||
version?: string
|
||||
has_update?: boolean
|
||||
latest_version?: string
|
||||
desc?: string
|
||||
file_url?: string
|
||||
}
|
||||
|
||||
export interface UpdateStartResponse {
|
||||
success: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function fetchVersion() {
|
||||
return requestGetJson<VersionResponse>('/api/version')
|
||||
}
|
||||
|
||||
export function startUpdate(fileUrl: string) {
|
||||
return requestPostJson<UpdateStartResponse>('/api/update/do', { file_url: fileUrl })
|
||||
}
|
||||
24
frontend-vue/src/shared/bridges/pywebview.ts
Normal file
24
frontend-vue/src/shared/bridges/pywebview.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface PywebviewApi {
|
||||
select_brand_xlsx_files?: () => Promise<string[]>
|
||||
select_brand_folder?: () => Promise<string | null>
|
||||
select_folder?: () => Promise<string | null>
|
||||
save_template_xlsx?: () => Promise<{ success: boolean; path?: string; error?: string }>
|
||||
save_template_zip?: () => Promise<{ success: boolean; path?: string; error?: string }>
|
||||
save_file_from_url?: (url: string, filename: string) => Promise<{ success: boolean; path?: string; error?: string }>
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
pywebview?: {
|
||||
api?: PywebviewApi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getPywebviewApi() {
|
||||
return window.pywebview?.api
|
||||
}
|
||||
|
||||
export function hasPywebview() {
|
||||
return Boolean(getPywebviewApi())
|
||||
}
|
||||
71
frontend-vue/src/shared/composables/useUpdateCheck.ts
Normal file
71
frontend-vue/src/shared/composables/useUpdateCheck.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ref } from 'vue'
|
||||
import { fetchVersion, startUpdate } from '@/shared/api/update'
|
||||
|
||||
export function useUpdateCheck() {
|
||||
const loading = ref(false)
|
||||
const version = ref('1.0.0')
|
||||
const hint = ref('')
|
||||
const downloadVisible = ref(false)
|
||||
const downloadLoading = ref(false)
|
||||
const fileUrl = ref('')
|
||||
|
||||
const check = async () => {
|
||||
loading.value = true
|
||||
hint.value = '正在检测更新...'
|
||||
downloadVisible.value = false
|
||||
|
||||
try {
|
||||
const result = await fetchVersion()
|
||||
version.value = (result.version || version.value).replace(/^v/i, '')
|
||||
|
||||
if (result.has_update && result.latest_version) {
|
||||
hint.value = `发现新版本 v${result.latest_version}${result.desc ? `:${result.desc}` : ''}`
|
||||
fileUrl.value = result.file_url || ''
|
||||
downloadVisible.value = true
|
||||
return
|
||||
}
|
||||
|
||||
hint.value = '已是最新版本'
|
||||
} catch (error) {
|
||||
hint.value = error instanceof Error ? error.message : '检测更新失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const runUpdate = async () => {
|
||||
if (!fileUrl.value) {
|
||||
hint.value = '暂无下载地址,请关注官方渠道。'
|
||||
return false
|
||||
}
|
||||
|
||||
downloadLoading.value = true
|
||||
hint.value = '正在下载并准备更新,程序将自动退出...'
|
||||
|
||||
try {
|
||||
const result = await startUpdate(fileUrl.value)
|
||||
if (result.success) {
|
||||
hint.value = '更新已启动,程序即将退出...'
|
||||
return true
|
||||
}
|
||||
|
||||
hint.value = result.error || '更新启动失败'
|
||||
return false
|
||||
} catch (error) {
|
||||
hint.value = error instanceof Error ? error.message : '请求更新失败,请重试'
|
||||
return false
|
||||
} finally {
|
||||
downloadLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
version,
|
||||
hint,
|
||||
downloadVisible,
|
||||
downloadLoading,
|
||||
check,
|
||||
runUpdate,
|
||||
}
|
||||
}
|
||||
16
frontend-vue/src/shared/utils/bootstrap.ts
Normal file
16
frontend-vue/src/shared/utils/bootstrap.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface BootstrapState {
|
||||
username?: string
|
||||
userId?: string
|
||||
isAdmin?: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__BOOTSTRAP__?: BootstrapState
|
||||
}
|
||||
}
|
||||
|
||||
export function getBootstrapState(): BootstrapState {
|
||||
return window.__BOOTSTRAP__ ?? {}
|
||||
}
|
||||
Reference in New Issue
Block a user