mirror of
https://github.com/wangdage12/Snap.Server.Web.git
synced 2026-02-17 10:02:08 +08:00
119
src/api/download.ts
Normal file
119
src/api/download.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/** 下载资源信息 */
|
||||
export interface DownloadResource {
|
||||
id?: string
|
||||
created_at: string
|
||||
created_by: string
|
||||
download_url: string
|
||||
features: string | null
|
||||
file_hash: string | null
|
||||
file_size: string | null
|
||||
is_active: boolean | null
|
||||
package_type: string
|
||||
version: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有发布的资源
|
||||
* GET /download-resources
|
||||
*/
|
||||
export function getDownloadResourcesApi(): Promise<DownloadResource[]> {
|
||||
return request({
|
||||
url: '/download-resources',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新版本
|
||||
* GET /download-resources/latest
|
||||
*/
|
||||
export function getLatestVersionApi(): Promise<DownloadResource> {
|
||||
return request({
|
||||
url: '/download-resources/latest',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源列表(包含未激活的)
|
||||
* GET /web-api/download-resources
|
||||
* @param package_type 筛选包类型(msi或者msix)
|
||||
* @param is_active 筛选是否激活
|
||||
*/
|
||||
export function getDownloadResourceListApi(params?: {
|
||||
package_type?: string
|
||||
is_active?: string
|
||||
}): Promise<DownloadResource[]> {
|
||||
return request({
|
||||
url: '/web-api/download-resources',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个资源详情
|
||||
* GET /web-api/download-resources/{resource_id}
|
||||
* @param resource_id 资源id
|
||||
*/
|
||||
export function getDownloadResourceDetailApi(resource_id: string): Promise<DownloadResource> {
|
||||
return request({
|
||||
url: `/web-api/download-resources/${resource_id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除下载资源
|
||||
* DELETE /web-api/download-resources/{resource_id}
|
||||
* @param resource_id 资源id
|
||||
*/
|
||||
export function deleteDownloadResourceApi(resource_id: string): Promise<null> {
|
||||
return request({
|
||||
url: `/web-api/download-resources/${resource_id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
||||
/** 创建资源请求参数类型 */
|
||||
export interface CreateResourceRequest {
|
||||
version: string
|
||||
package_type: string
|
||||
download_url: string
|
||||
features?: string | null
|
||||
file_size?: string | null
|
||||
file_hash?: string | null
|
||||
is_active?: boolean | null
|
||||
}
|
||||
|
||||
/** 创建资源响应数据类型 */
|
||||
export interface CreateResourceResponse {
|
||||
id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新版本资源信息
|
||||
* POST /web-api/download-resources
|
||||
*/
|
||||
export function createDownloadResourceApi(params: CreateResourceRequest): Promise<CreateResourceResponse> {
|
||||
return request({
|
||||
url: '/web-api/download-resources',
|
||||
method: 'post',
|
||||
data: params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新下载资源
|
||||
* PUT /web-api/download-resources/{resource_id}
|
||||
* @param resource_id 资源id
|
||||
*/
|
||||
export function updateDownloadResourceApi(resource_id: string, params: Partial<CreateResourceRequest>): Promise<null> {
|
||||
return request({
|
||||
url: `/web-api/download-resources/${resource_id}`,
|
||||
method: 'put',
|
||||
data: params,
|
||||
})
|
||||
}
|
||||
@@ -12,6 +12,11 @@ const routes = [
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: { hidden: true }
|
||||
},
|
||||
{
|
||||
path: '/download',
|
||||
component: () => import('@/views/download/index.vue'),
|
||||
meta: { hidden: true }
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: DefaultLayout,
|
||||
@@ -47,6 +52,11 @@ const routes = [
|
||||
component: () => import('@/views/announcement/index.vue'),
|
||||
meta: { title: '公告管理', icon: 'Bell' },
|
||||
},
|
||||
{
|
||||
path: 'download-manager',
|
||||
component: () => import('@/views/download-manager/index.vue'),
|
||||
meta: { title: '下载资源管理', icon: 'Download' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -6,8 +6,8 @@ router.beforeEach(async (to, _ , next) => {
|
||||
|
||||
// 未登录
|
||||
if (!userStore.token) {
|
||||
// 主页(/)允许未登录访问
|
||||
if (to.path === '/' || to.path === '/login') {
|
||||
// 主页(/)、登录页和下载页允许未登录访问
|
||||
if (to.path === '/' || to.path === '/login' || to.path === '/download') {
|
||||
next()
|
||||
} else {
|
||||
next('/login')
|
||||
|
||||
@@ -2,13 +2,13 @@ import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const request = axios.create({
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
/** 请求拦截:自动加 Token */
|
||||
request.interceptors.request.use((config) => {
|
||||
axiosInstance.interceptors.request.use((config: any) => {
|
||||
const userStore = useUserStore()
|
||||
if (userStore.token) {
|
||||
config.headers = config.headers || {}
|
||||
@@ -18,8 +18,8 @@ request.interceptors.request.use((config) => {
|
||||
})
|
||||
|
||||
/** 响应拦截:兼容 code / retcode */
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response: any) => {
|
||||
const res = response.data
|
||||
|
||||
// 登录接口:code
|
||||
@@ -43,7 +43,7 @@ request.interceptors.response.use(
|
||||
// 兜底
|
||||
return res
|
||||
},
|
||||
(error) => {
|
||||
(error: any) => {
|
||||
// 处理401未授权错误
|
||||
if (error.response?.status === 401) {
|
||||
const userStore = useUserStore()
|
||||
@@ -63,4 +63,4 @@ request.interceptors.response.use(
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
export default axiosInstance
|
||||
641
src/views/download-manager/index.vue
Normal file
641
src/views/download-manager/index.vue
Normal file
@@ -0,0 +1,641 @@
|
||||
<template>
|
||||
<div class="resource-management">
|
||||
<!-- 搜索栏和统计 -->
|
||||
<div class="search-statistics-row">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="包类型">
|
||||
<el-select
|
||||
v-model="searchForm.package_type"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="MSI" value="msi" />
|
||||
<el-option label="MSIX" value="msix" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="激活状态">
|
||||
<el-select
|
||||
v-model="searchForm.is_active"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="已激活" value="true" />
|
||||
<el-option label="未激活" value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :loading="loading">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="statistics" v-if="!loading && resourceList.length > 0">
|
||||
<el-statistic title="资源总数" :value="resourceList.length" />
|
||||
<el-statistic title="MSI包" :value="msiCount" />
|
||||
<el-statistic title="MSIX包" :value="msixCount" />
|
||||
<el-statistic title="已激活" :value="activeCount" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="toolbar">
|
||||
<el-button type="success" @click="handleCreate">创建资源</el-button>
|
||||
<el-button type="primary" @click="handleRefresh" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 资源表格 -->
|
||||
<el-table
|
||||
:data="resourceList"
|
||||
style="width: 100%"
|
||||
border
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载资源数据..."
|
||||
>
|
||||
<el-table-column prop="version" label="版本号" width="120" />
|
||||
<el-table-column label="包类型" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.package_type === 'msix' ? 'success' : 'primary'" size="small">
|
||||
{{ scope.row.package_type.toUpperCase() }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="激活状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.is_active ? 'success' : 'info'" size="small">
|
||||
{{ scope.row.is_active ? '已激活' : '未激活' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="file_size" label="文件大小" width="120" />
|
||||
<el-table-column label="下载链接" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-link
|
||||
:href="scope.row.download_url"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
>
|
||||
{{ scope.row.download_url }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="features" label="新功能描述" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatTime(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleView(scope.row)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="warning"
|
||||
link
|
||||
@click="handleEdit(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-if="!loading && resourceList.length === 0" description="暂无资源数据" />
|
||||
|
||||
<!-- 资源详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="资源详情"
|
||||
width="60%"
|
||||
>
|
||||
<div v-if="currentResource" class="resource-detail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="版本号">
|
||||
<el-tag type="primary">{{ currentResource.version }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="包类型">
|
||||
<el-tag :type="currentResource.package_type === 'msix' ? 'success' : 'primary'">
|
||||
{{ currentResource.package_type.toUpperCase() }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="激活状态">
|
||||
<el-tag :type="currentResource.is_active ? 'success' : 'info'">
|
||||
{{ currentResource.is_active ? '已激活' : '未激活' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件大小">
|
||||
{{ currentResource.file_size || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件哈希" :span="2">
|
||||
{{ currentResource.file_hash || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建者ID">
|
||||
{{ currentResource.created_by }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ formatTime(currentResource.created_at) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="下载链接" :span="2">
|
||||
<el-link
|
||||
:href="currentResource.download_url"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
{{ currentResource.download_url }}
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="新功能描述" :span="2">
|
||||
<div class="features-content">
|
||||
{{ currentResource.features || '无' }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 创建资源弹窗 -->
|
||||
<el-dialog
|
||||
v-model="createDialogVisible"
|
||||
title="创建资源"
|
||||
width="60%"
|
||||
>
|
||||
<el-form
|
||||
ref="createFormRef"
|
||||
:model="createForm"
|
||||
:rules="createRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="版本号" prop="version">
|
||||
<el-input
|
||||
v-model="createForm.version"
|
||||
placeholder="请输入版本号,如:1.0.0"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="包类型" prop="package_type">
|
||||
<el-select
|
||||
v-model="createForm.package_type"
|
||||
placeholder="请选择包类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="MSI" value="msi" />
|
||||
<el-option label="MSIX" value="msix" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="下载链接" prop="download_url">
|
||||
<el-input
|
||||
v-model="createForm.download_url"
|
||||
placeholder="请输入下载URL"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="文件大小">
|
||||
<el-input
|
||||
v-model="createForm.file_size"
|
||||
placeholder="可选,如:114514KB"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="文件哈希">
|
||||
<el-input
|
||||
v-model="createForm.file_hash"
|
||||
placeholder="可选,文件哈希值"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="激活状态">
|
||||
<el-switch
|
||||
v-model="createForm.is_active"
|
||||
active-text="已激活"
|
||||
inactive-text="未激活"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新功能描述">
|
||||
<el-input
|
||||
v-model="createForm.features"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="可选,描述新功能内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="createLoading"
|
||||
@click="handleCreateSubmit"
|
||||
>
|
||||
创建
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑资源弹窗 -->
|
||||
<el-dialog
|
||||
v-model="editDialogVisible"
|
||||
title="编辑资源"
|
||||
width="60%"
|
||||
>
|
||||
<el-form
|
||||
ref="editFormRef"
|
||||
:model="editForm"
|
||||
:rules="editRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="版本号" prop="version">
|
||||
<el-input
|
||||
v-model="editForm.version"
|
||||
placeholder="请输入版本号,如:1.0.0"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="包类型" prop="package_type">
|
||||
<el-select
|
||||
v-model="editForm.package_type"
|
||||
placeholder="请选择包类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="MSI" value="msi" />
|
||||
<el-option label="MSIX" value="msix" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="下载链接" prop="download_url">
|
||||
<el-input
|
||||
v-model="editForm.download_url"
|
||||
placeholder="请输入下载URL"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="文件大小">
|
||||
<el-input
|
||||
v-model="editForm.file_size"
|
||||
placeholder="可选,如:114514KB"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="文件哈希">
|
||||
<el-input
|
||||
v-model="editForm.file_hash"
|
||||
placeholder="可选,文件哈希值"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="激活状态">
|
||||
<el-switch
|
||||
v-model="editForm.is_active"
|
||||
active-text="已激活"
|
||||
inactive-text="未激活"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新功能描述">
|
||||
<el-input
|
||||
v-model="editForm.features"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="可选,描述新功能内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="editLoading"
|
||||
@click="handleEditSubmit"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import {
|
||||
getDownloadResourceListApi,
|
||||
deleteDownloadResourceApi,
|
||||
createDownloadResourceApi,
|
||||
updateDownloadResourceApi,
|
||||
type DownloadResource,
|
||||
type CreateResourceRequest,
|
||||
} from '@/api/download'
|
||||
|
||||
interface SearchForm {
|
||||
package_type: string
|
||||
is_active: string
|
||||
}
|
||||
|
||||
const searchForm = reactive<SearchForm>({
|
||||
package_type: '',
|
||||
is_active: '',
|
||||
})
|
||||
|
||||
const resourceList = ref<DownloadResource[]>([])
|
||||
const loading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentResource = ref<DownloadResource | null>(null)
|
||||
|
||||
// 创建资源相关
|
||||
const createDialogVisible = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createFormRef = ref<FormInstance>()
|
||||
|
||||
const createForm = reactive<CreateResourceRequest>({
|
||||
version: '',
|
||||
package_type: 'msix',
|
||||
download_url: '',
|
||||
features: '',
|
||||
file_size: '',
|
||||
file_hash: '',
|
||||
is_active: true,
|
||||
})
|
||||
|
||||
const createRules: FormRules = {
|
||||
version: [
|
||||
{ required: true, message: '请输入版本号', trigger: 'blur' },
|
||||
{ pattern: /^\d+\.\d+\.\d+$/, message: '版本号格式应为 x.y.z,如 1.0.0', trigger: 'blur' },
|
||||
],
|
||||
package_type: [
|
||||
{ required: true, message: '请选择包类型', trigger: 'change' },
|
||||
],
|
||||
download_url: [
|
||||
{ required: true, message: '请输入下载链接', trigger: 'blur' },
|
||||
{ type: 'url', message: '请输入有效的URL地址', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
// 编辑资源相关
|
||||
const editDialogVisible = ref(false)
|
||||
const editLoading = ref(false)
|
||||
const editFormRef = ref<FormInstance>()
|
||||
const currentEditId = ref<string | null>(null)
|
||||
|
||||
const editForm = reactive<CreateResourceRequest>({
|
||||
version: '',
|
||||
package_type: 'msix',
|
||||
download_url: '',
|
||||
features: '',
|
||||
file_size: '',
|
||||
file_hash: '',
|
||||
is_active: true,
|
||||
})
|
||||
|
||||
const editRules: FormRules = {
|
||||
version: [
|
||||
{ required: true, message: '请输入版本号', trigger: 'blur' },
|
||||
{ pattern: /^\d+\.\d+\.\d+$/, message: '版本号格式应为 x.y.z,如 1.0.0', trigger: 'blur' },
|
||||
],
|
||||
package_type: [
|
||||
{ required: true, message: '请选择包类型', trigger: 'change' },
|
||||
],
|
||||
download_url: [
|
||||
{ required: true, message: '请输入下载链接', trigger: 'blur' },
|
||||
{ type: 'url', message: '请输入有效的URL地址', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
const msiCount = computed(() =>
|
||||
resourceList.value.filter(item => item.package_type === 'msi').length
|
||||
)
|
||||
|
||||
const msixCount = computed(() =>
|
||||
resourceList.value.filter(item => item.package_type === 'msix').length
|
||||
)
|
||||
|
||||
const activeCount = computed(() =>
|
||||
resourceList.value.filter(item => item.is_active === true).length
|
||||
)
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(dateStr: string) {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// 获取资源列表
|
||||
async function fetchResourceList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: Record<string, any> = {}
|
||||
if (searchForm.package_type) {
|
||||
params.package_type = searchForm.package_type
|
||||
}
|
||||
if (searchForm.is_active) {
|
||||
params.is_active = searchForm.is_active
|
||||
}
|
||||
|
||||
const data = await getDownloadResourceListApi(Object.keys(params).length > 0 ? params : undefined)
|
||||
resourceList.value = data || []
|
||||
} catch (error) {
|
||||
console.error('获取资源列表失败:', error)
|
||||
ElMessage.error('获取资源列表失败')
|
||||
resourceList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
fetchResourceList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.package_type = ''
|
||||
searchForm.is_active = ''
|
||||
fetchResourceList()
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
fetchResourceList()
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
// 重置表单
|
||||
Object.assign(createForm, {
|
||||
version: '',
|
||||
package_type: 'msix',
|
||||
download_url: '',
|
||||
features: '',
|
||||
file_size: '',
|
||||
file_hash: '',
|
||||
is_active: true,
|
||||
})
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleCreateSubmit() {
|
||||
if (!createFormRef.value) return
|
||||
|
||||
try {
|
||||
await createFormRef.value.validate()
|
||||
createLoading.value = true
|
||||
|
||||
const result = await createDownloadResourceApi(createForm)
|
||||
|
||||
if (result && result.id) {
|
||||
ElMessage.success('资源创建成功')
|
||||
createDialogVisible.value = false
|
||||
// 刷新列表
|
||||
await fetchResourceList()
|
||||
} else {
|
||||
ElMessage.error('创建资源失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('创建资源失败')
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit(resource: DownloadResource) {
|
||||
if (!resource.id) {
|
||||
ElMessage.error('资源ID不存在,无法编辑')
|
||||
return
|
||||
}
|
||||
|
||||
currentEditId.value = resource.id
|
||||
// 填充表单数据
|
||||
Object.assign(editForm, {
|
||||
version: resource.version,
|
||||
package_type: resource.package_type,
|
||||
download_url: resource.download_url,
|
||||
features: resource.features || '',
|
||||
file_size: resource.file_size || '',
|
||||
file_hash: resource.file_hash || '',
|
||||
is_active: resource.is_active ?? true,
|
||||
})
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEditSubmit() {
|
||||
if (!editFormRef.value || !currentEditId.value) return
|
||||
|
||||
try {
|
||||
await editFormRef.value.validate()
|
||||
editLoading.value = true
|
||||
|
||||
await updateDownloadResourceApi(currentEditId.value, editForm)
|
||||
|
||||
ElMessage.success('资源更新成功')
|
||||
editDialogVisible.value = false
|
||||
// 刷新列表
|
||||
await fetchResourceList()
|
||||
} catch (error) {
|
||||
ElMessage.error('更新资源失败')
|
||||
} finally {
|
||||
editLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleView(resource: DownloadResource) {
|
||||
currentResource.value = resource
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleDelete(resource: DownloadResource) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除版本 ${resource.version} 的 ${resource.package_type.toUpperCase()} 包吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
|
||||
if (!resource.id) {
|
||||
ElMessage.error('资源ID不存在,无法删除')
|
||||
return
|
||||
}
|
||||
|
||||
await deleteDownloadResourceApi(resource.id)
|
||||
|
||||
ElMessage.success('资源删除成功')
|
||||
await fetchResourceList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除资源失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchResourceList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.resource-management {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.search-statistics-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.statistics {
|
||||
margin-left: 32px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.features-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
557
src/views/download/index.vue
Normal file
557
src/views/download/index.vue
Normal file
@@ -0,0 +1,557 @@
|
||||
<template>
|
||||
<div class="download-page">
|
||||
<Header
|
||||
:app-icon="appIcon"
|
||||
:app-name="appName"
|
||||
:actions="headerActions"
|
||||
/>
|
||||
|
||||
<div class="download-content">
|
||||
<div class="download-section">
|
||||
<h1 class="section-title">
|
||||
<el-icon><Download /></el-icon>
|
||||
下载中心
|
||||
</h1>
|
||||
<p class="section-description">选择适合您的安装包,立即开始使用 Snap Hutao<br>系统要求:新版本Windows10及Windows11</p>
|
||||
|
||||
|
||||
<!-- 最新版本下载区域 -->
|
||||
<div v-if="latestVersion" class="latest-version-card">
|
||||
<div class="latest-header">
|
||||
<el-tag type="success" size="large" effect="dark">最新版本</el-tag>
|
||||
<h2 class="version-title">{{ latestVersion.version }}</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="latestVersion.features" class="features-section">
|
||||
<h3 class="features-title">更新内容</h3>
|
||||
<p class="features-text">{{ latestVersion.features }}</p>
|
||||
</div>
|
||||
|
||||
<div class="download-buttons">
|
||||
<el-tooltip
|
||||
v-if="latestVersion.packages.msi"
|
||||
effect="dark"
|
||||
placement="top"
|
||||
:content="getTooltipText(latestVersion, 'msi')"
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="downloadFile(latestVersion, 'msi')"
|
||||
:loading="downloading"
|
||||
>
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>下载 MSI 安装包</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="latestVersion.packages.msix"
|
||||
effect="dark"
|
||||
placement="top"
|
||||
:content="getTooltipText(latestVersion, 'msix')"
|
||||
>
|
||||
<el-button
|
||||
type="success"
|
||||
size="large"
|
||||
@click="downloadFile(latestVersion, 'msix')"
|
||||
:loading="downloading"
|
||||
>
|
||||
<el-icon><Box /></el-icon>
|
||||
<span>下载 MSIX 安装包</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="version-info">
|
||||
<span class="info-item">
|
||||
<el-icon><Clock /></el-icon>
|
||||
发布时间:{{ formatDate(latestVersion.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider>历史版本</el-divider>
|
||||
|
||||
<!-- 历史版本列表 -->
|
||||
<div v-loading="loading" class="history-list">
|
||||
<div v-if="historyVersions.length === 0 && !loading" class="empty-state">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
<p>暂无历史版本</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="version-table">
|
||||
<div
|
||||
v-for="(item, index) in historyVersions"
|
||||
:key="index"
|
||||
class="version-item"
|
||||
>
|
||||
<div class="version-main">
|
||||
<div class="version-number">{{ item.version }}</div>
|
||||
<div class="version-meta">
|
||||
<span class="meta-item">
|
||||
<el-icon><Clock /></el-icon>
|
||||
{{ formatDate(item.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="item.features" class="version-features">
|
||||
{{ item.features }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-actions">
|
||||
<el-tooltip
|
||||
v-if="item.packages.msi"
|
||||
effect="dark"
|
||||
placement="top"
|
||||
:content="getTooltipText(item, 'msi')"
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="downloadFile(item, 'msi')"
|
||||
>
|
||||
MSI
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="item.packages.msix"
|
||||
effect="dark"
|
||||
placement="top"
|
||||
:content="getTooltipText(item, 'msix')"
|
||||
>
|
||||
<el-button
|
||||
type="success"
|
||||
size="small"
|
||||
@click="downloadFile(item, 'msix')"
|
||||
>
|
||||
MSIX
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Download,
|
||||
Document,
|
||||
Box,
|
||||
Clock,
|
||||
FolderOpened,
|
||||
} from '@element-plus/icons-vue'
|
||||
import Header from '@/components/Header.vue'
|
||||
import { getDownloadResourcesApi } from '@/api/download'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 配置项
|
||||
const appIcon = ref('/HT_logo.png')
|
||||
const appName = ref('Snap Hutao')
|
||||
|
||||
// 页头右侧按钮配置
|
||||
const headerActions = ref([
|
||||
{
|
||||
id: 'home',
|
||||
label: '返回首页',
|
||||
icon: undefined,
|
||||
component: 'el-button',
|
||||
props: {
|
||||
type: 'default',
|
||||
link: true
|
||||
},
|
||||
onClick: () => {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 版本信息接口(合并不同包类型)
|
||||
interface VersionInfo {
|
||||
version: string
|
||||
created_at: string
|
||||
created_by: string
|
||||
features: string | null
|
||||
file_hash: string | null
|
||||
file_size: string | null
|
||||
is_active: boolean | null
|
||||
packages: {
|
||||
msi: string | null
|
||||
msix: string | null
|
||||
msi_size: string | null
|
||||
msix_size: string | null
|
||||
}
|
||||
}
|
||||
|
||||
// 包类型描述
|
||||
const packageDescriptions = {
|
||||
msi: '传统安装包,方便部署,但是可能有BUG。',
|
||||
msix: '现代化安装包,稳定性更好,原生体验,推荐使用,安装稍繁琐,解压以后右键Add-AppDevPackage.ps1,选择“以PowerShell运行”进行安装。',
|
||||
}
|
||||
|
||||
// 数据状态
|
||||
const latestVersion = ref<VersionInfo | null>(null)
|
||||
const historyVersions = ref<VersionInfo[]>([])
|
||||
const loading = ref(false)
|
||||
const downloading = ref(false)
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
function formatDate(dateStr: string) {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
function downloadFile(item: VersionInfo, packageType: 'msi' | 'msix') {
|
||||
const downloadUrl = item.packages[packageType]
|
||||
if (!downloadUrl) {
|
||||
ElMessage.warning(`${item.version} 版本暂无 ${packageType.toUpperCase()} 安装包`)
|
||||
return
|
||||
}
|
||||
|
||||
downloading.value = true
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = downloadUrl
|
||||
a.download = `Snap.Hutao.${item.version}.${packageType}`
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
|
||||
ElMessage.success(`开始下载 ${item.version} 版本的 ${packageType.toUpperCase()} 安装包`)
|
||||
|
||||
setTimeout(() => {
|
||||
downloading.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载按钮的提示内容
|
||||
*/
|
||||
function getTooltipText(item: VersionInfo, packageType: 'msi' | 'msix') {
|
||||
const size = item.packages[`${packageType}_size` as keyof typeof item.packages] as string | null
|
||||
const desc = packageDescriptions[packageType]
|
||||
|
||||
if (size) {
|
||||
return `${desc}\n文件大小:${size}`
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载所有版本
|
||||
*/
|
||||
async function loadAllVersions() {
|
||||
try {
|
||||
loading.value = true
|
||||
const data = await getDownloadResourcesApi()
|
||||
if (!data || data.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 按版本号分组
|
||||
const versionMap = new Map<string, VersionInfo>()
|
||||
|
||||
data.forEach((item) => {
|
||||
if (!versionMap.has(item.version)) {
|
||||
// 首次遇到该版本,创建新记录
|
||||
versionMap.set(item.version, {
|
||||
version: item.version,
|
||||
created_at: item.created_at,
|
||||
created_by: item.created_by,
|
||||
features: item.features,
|
||||
file_hash: item.file_hash,
|
||||
file_size: item.file_size,
|
||||
is_active: item.is_active,
|
||||
packages: {
|
||||
msi: null,
|
||||
msix: null,
|
||||
msi_size: null,
|
||||
msix_size: null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 添加包类型的下载链接和大小
|
||||
const versionInfo = versionMap.get(item.version)
|
||||
if (versionInfo) {
|
||||
if (item.package_type === 'msi') {
|
||||
versionInfo.packages.msi = item.download_url
|
||||
versionInfo.packages.msi_size = item.file_size
|
||||
} else if (item.package_type === 'msix') {
|
||||
versionInfo.packages.msix = item.download_url
|
||||
versionInfo.packages.msix_size = item.file_size
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 转换为数组并按创建时间倒序排序
|
||||
const versions = Array.from(versionMap.values()).sort((a, b) =>
|
||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
)
|
||||
|
||||
if (versions.length > 0) {
|
||||
latestVersion.value = versions[0] ?? null
|
||||
historyVersions.value = versions.slice(1)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载版本列表失败:', error)
|
||||
ElMessage.error('加载版本列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAllVersions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.download-page {
|
||||
min-height: 100vh;
|
||||
background: var(--main-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.download-content {
|
||||
flex: 1;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.download-section {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-color);
|
||||
background: linear-gradient(135deg, var(--aside-active), #67c23a);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
font-size: 16px;
|
||||
color: var(--text-color);
|
||||
opacity: 0.8;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 最新版本卡片 */
|
||||
.latest-version-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.latest-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.version-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.features-text {
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
opacity: 0.8;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
background: var(--main-bg);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.download-buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.download-buttons .el-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px 32px;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
min-width: 200px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 历史版本列表 */
|
||||
.history-list {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state .el-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.version-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.version-item {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.version-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.version-main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.version-number {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.version-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.version-features {
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
opacity: 0.8;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
background: var(--main-bg);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
max-height: 80px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.version-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.section-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.latest-version-card {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.download-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.download-buttons .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.version-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.version-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -90,18 +90,28 @@ const headerActions = ref([
|
||||
|
||||
// 主页中心按钮配置
|
||||
const heroButtons = ref([
|
||||
// {
|
||||
// id: 'btn1',
|
||||
// label: '快速开始',
|
||||
// type: 'primary',
|
||||
// size: 'large',
|
||||
// icon: undefined,
|
||||
// onClick: () => {
|
||||
// window.open('https://github.com/wangdage12/Snap.Hutao', '_blank')
|
||||
// }
|
||||
// },
|
||||
{
|
||||
id: 'btn1',
|
||||
label: '快速开始',
|
||||
id: 'btn2',
|
||||
label: '立即下载',
|
||||
type: 'primary',
|
||||
size: 'large',
|
||||
icon: undefined,
|
||||
onClick: () => {
|
||||
window.open('https://github.com/wangdage12/Snap.Hutao', '_blank')
|
||||
router.push('/download')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'btn2',
|
||||
id: 'btn3',
|
||||
label: '查看文档',
|
||||
type: 'default',
|
||||
size: 'large',
|
||||
|
||||
@@ -94,7 +94,7 @@ const handleLogin = async () => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 处理响应数据结构
|
||||
const tokenData = response.data || response
|
||||
const tokenData = response as any
|
||||
userStore.setToken(tokenData.access_token)
|
||||
|
||||
// 获取用户信息
|
||||
|
||||
Reference in New Issue
Block a user