Files
lionwebsite-frontend-mobile/src/store/index.js
T
lion 169925c197 同步桌面端更新:重连/重试、断线自动重连、图片容错与鉴权参数
移植桌面端 2025-03-22 之后的更新到移动端,保留移动端自身布局:

- 补齐管理子路径鉴权参数:缩略图、在线图片、/cache 请求带上 AuthCode;
  原线上靠临时热补丁脚本 online-reader-auth-fix.js 绕过,现由源码原生支持。
- 断线自动重连:新增 src/utils/taskSocket.js,指数退避重连并在重连后重新同步
  任务列表与用量;App.vue 接入页面可见性/网络恢复事件,Side.vue 显示连接状态。
- 未完成任务重试:新增 retryGallery action 与重试按钮;补 "等待压缩" 状态。
- 图片容错:新增 src/utils/progressiveImages.js,单图加载失败不再阻塞后续页,
  支持单图重试。
- store 修复:收藏列表残留(slice→splice)、action 内改 context.state、
  gid 判空(null→undefined)、deleteTask 跳过下一元素、resetUndone 用 state 授权码、
  axios 全局错误拦截器、getShortname 简化。
- 下载链接统一由 buildGalleryDownloadUrl 生成(按 gid 而非名字)。
- 构建配置:base 改为 /asserts/mobile/、固定产物名 index.js/index.css、新增 .gitignore。
- 升级 Vite 8.3.0 + @vitejs/plugin-vue 6.0.9,npm audit 0 漏洞。
- 新增 tests/(11 项 Node 测试,全部通过)。
2026-09-20 14:03:10 +08:00

560 lines
20 KiB
JavaScript

import vuex from "vuex"
import axios from "axios"
import {ElMessage} from "element-plus"
import qs from "qs"
import {createTaskSocket} from "../utils/taskSocket.js"
let taskSocket
let taskRefreshVersion = 0
const BaseUrl = "https://downloader.lionwebsite.xyz/"
const GalleryManageUrl = BaseUrl + "GalleryManage"
// Axios 全局错误处理
axios.interceptors.response.use(
response => response,
error => {
ElMessage({message: "网络请求失败: " + (error.message || ""), type: "error"})
return Promise.reject(error)
}
)
const actions = {
reconnect(context) {
axios.post(GalleryManageUrl + "/reconnect", null, {
params: {
AuthCode: context.state.AuthCode
}
}).then(res => {
if (res.data.result === "success") {
ElMessage("重连成功")
} else {
ElMessage("重连失败")
}
})
},
updateGalleryTasks(context){
const version = ++taskRefreshVersion
return axios.get(GalleryManageUrl, {
params:{
AuthCode: context.state.AuthCode,
type: 'all'
}
}).then((res) => {
if(res.data.result === "success" && version === taskRefreshVersion)
context.commit("_updateGalleryTasks", JSON.parse(res.data.data))
})
},
postGalleryTask(context, data){
return axios.post(GalleryManageUrl + '?' + qs.stringify({
AuthCode: context.state.AuthCode,
...data
}, {indices:false})).then((res) => {
if(res.data.result === "success") {
ElMessage("提交成功")
context.commit("_setChosenGallery", {gallery: false})
}
else
if(res.data.data)
ElMessage(res.data.data)
else
ElMessage("提交失败")
// A node timeout can still leave a persisted task; always reload its actual state.
return Promise.all([context.dispatch("updateGalleryTasks"), context.dispatch("loadWeekUsedAmount")])
})
},
queryGalleryTask(context, link){
axios.get(GalleryManageUrl, {
params:{
param: link,
type: 'link',
AuthCode: context.state.AuthCode
}
}).then((res) => {
if(res.data.result === 'success'){
let gallery = JSON.parse(res.data.data)
if(gallery.status === "下载完成")
gallery.download = buildGalleryDownloadUrl(gallery.gid, context.state.AuthCode)
context.commit("_setChosenGallery", {gallery})
}
else
ElMessage("查询失败")
})
},
validate(context, AuthCode){
axios.post(BaseUrl + "validate?AuthCode=" + AuthCode).then((res)=>{
if(res.data.result === 'success'){
let data = JSON.parse(res.data.data);
if(!data.isAvailable)
ElMessage({duration:0, message:"节点挂了,不能下也不能看,找狮子处理", type: "error"})
context.commit("_authed", {AuthCode, ...data})
//初始化
context.dispatch("loadWeekUsedAmount").then()
context.dispatch("updateGalleryTasks").then(() => confirmCurrentTask(context.state))
context.dispatch("initWebsocket").then()
}
else {
context.dispatch("disconnectWebsocket")
context.commit("_unAuthed")
}
})
},
initWebsocket(context){
taskSocket?.stop()
taskSocket = createTaskSocket({
url: "wss://downloader.lionwebsite.xyz/ws/",
onState: status => context.commit("_setConnectionStatus", status),
onOpen: () => {
// Catch up on every connection, including events missed during an outage.
Promise.all([context.dispatch("updateGalleryTasks"), context.dispatch("loadWeekUsedAmount")]).catch(() => {})
},
onMessage: event => {
let message
try { message = JSON.parse(event.data) }
catch { return }
if(message.type === "updateTasks" && Array.isArray(message.data))
context.commit("_updateGalleryTaskProceeding", message.data)
else if(message.type === "fullUpdate")
context.dispatch("updateGalleryTasks").catch(() => {})
}
})
taskSocket.start()
},
disconnectWebsocket(context){
taskSocket?.stop()
taskSocket = undefined
context.commit("_setConnectionStatus", "disconnected")
},
loadWeekUsedAmount(context){
return axios.get(GalleryManageUrl + "/weekUsedAmount", {
params: {
AuthCode: context.state.AuthCode
}
}).then((res) => {
if(res.data.result === "success"){
context.state.weekUsed = JSON.parse(res.data.data)
}else
ElMessage("查询用量失败")
})
},
collectGallery(context, gid){
axios.post(GalleryManageUrl + "/collect?" +qs.stringify( {
gid, AuthCode:context.state.AuthCode
})).then((res) => {
ElMessage(res.data.data)
if(res.data.result === 'success')
context.commit("_collectGallery", gid)
})
},
disCollectGallery(context, gid){
axios.post(GalleryManageUrl + "/disCollect?" + qs.stringify({
gid, AuthCode:context.state.AuthCode
})).then((res) => {
ElMessage(res.data.data)
if(res.data.result === 'success')
context.commit("_disCollectGallery", gid)
})
},
deleteGallery(context, gid){
return axios.delete(GalleryManageUrl, {
params:{
AuthCode:context.state.AuthCode, gid
}}).then((res) => {
if(res.data.result === "success"){
ElMessage("删除成功")
context.commit("_deleteGallery", gid)
}
else
ElMessage(res.data.data)
})
},
retryGallery(context, gid){
return axios.post(GalleryManageUrl + "/retry?" + qs.stringify({
gid, AuthCode: context.state.AuthCode
})).then((res) => {
if(res.data.result === "success"){
ElMessage({message: "重试结果:" + res.data.data, type: "success"})
return context.dispatch("updateGalleryTasks")
}
ElMessage({message: res.data.data || "重试失败", type: "error"})
})
},
readOnlineGallery(context, gallery){
if(gallery.images !== undefined && gallery.images.length !== 0)
context.commit("_setReadingGallery", gallery)
else
return axios.post(GalleryManageUrl + "/cache", null, {
params: {url: gallery.link, AuthCode: context.state.AuthCode}
}).then((res) => {
if (res.data.result === 'success') {
gallery.pages = res.data.data.pages
setTimeout(() => {
context.commit("_setReadingGallery", gallery)
}, 100)
} else
ElMessage.error(res.data.data)
})
},
alterAuthCode(context, AuthCode){
axios.put(BaseUrl + "AuthCode?" + qs.stringify({'AuthCode': context.state.AuthCode, 'newAuthCode': AuthCode}))
.then((res) => {
if(res.data.result === 'success') {
ElMessage("修改成功")
if(localStorage.getItem("auth") === context.state.AuthCode)
localStorage.setItem("auth", AuthCode)
context.state.AuthCode = AuthCode
}
else
ElMessage(res.data.data)
})
},
resetUndone(context){
axios.post(GalleryManageUrl + "/reset?AuthCode=" + context.state.AuthCode).then((res) => {
ElMessage(res.data.data)
})
}
}
const mutations = {
_setConnectionStatus(state, status){
state.connectionStatus = status
},
_collectGallery(state, gid){
let tasks = state.totalGalleryTask
for(let i=0; i< tasks.length; i++){
if(!tasks[i].isCollect && tasks[i].gid === gid){
tasks[i].isCollect = true
state.collectGallery.push(tasks[i])
}
}
},
_disCollectGallery(state, gid){
let index
for(let i=0; i < state.collectGallery.length; i++)
if(state.collectGallery[i].gid === gid){
index = i
break
}
state.collectGallery[index].isCollect = false
state.collectGallery.splice(index, 1)
},
_updateGalleryTasks(state, tasks){
state.totalGalleryTask.splice(0)
state.collectGallery.splice(0)
state.downloadGallery.splice(0)
tasks.forEach((task) => {
//处理名字
task.shortName = getShortname(task.name)
if(task.thumb_link.trim() !== '')
task.thumb_link = buildGalleryManageUrl("/ehThumbnail", {
path: task.thumb_link,
AuthCode: state.AuthCode
})
else
delete task.thumb_link
//处理进度相关
switch (task.status) {
case "已提交":
case "等待压缩":
case "压缩中":
task.progress = task.status
break;
case "下载中":
task.progress = (Math.round((task.proceeding / task.pages) * 100)).toString() + "%"
break;
case "下载完成":
task.progress = task.status
task.download = buildGalleryDownloadUrl(task.gid, state.AuthCode)
break;
}
//处理时间戳
task.createTimeDisplay = new Date(task.createTime * 1000).toLocaleString("zh")
//处理是否收藏
if('isCollect' in task)
state.collectGallery.push(task)
else
task.isCollect = false
//处理是否下载
if(task.downloader === state.userId)
state.downloadGallery.push(task)
state.totalGalleryTask.push(task)
})
sortTasks(state)
if(state.isAuth && !state.loadComplete){
state.loadComplete = true
ElMessage("加载完成")
}
},
_updateGalleryTaskProceeding(state, tasks){
let galleries = Array.from(state.totalGalleryTask)
state.totalGalleryTask.splice(0)
galleries.forEach((gallery) => {
if(gallery.status !== '下载完成')
tasks.forEach((task) => {
if(task.gid === gallery.gid){
gallery.status = status[task.status]
gallery.proceeding = task.proceeding
if(gallery.status === '下载中')
gallery.progress = (Math.round((gallery.proceeding / gallery.pages) * 100)).toString() + "%"
else
gallery.progress = gallery.status
}
})
state.totalGalleryTask.push(gallery)
})
},
_changePage(state, targetPage){
state.page = targetPage
},
_authed(state, data){
state.AuthCode = data.AuthCode
state.userId = data.userId
state.username = data.username
state.isAuth = true
ElMessage("验证成功,加载中")
},
_unAuthed(state){
state.isAuth = false
state.AuthCode = ""
state.userId = -1
state.username = ""
ElMessage("授权码错误")
localStorage.removeItem("auth")
},
_searchLocalByLink(state, link){
let tasks = state.currentTasks
let gid = link.split("/")[4]
let name = null
if(gid === undefined)
for (let i = 0; i < tasks.length; i++) {
if (tasks[i].link === link) {
state.page = Math.floor(i / state.length) + 1
name = tasks[i].name
break
}
}
else for (let i = 0; i < tasks.length; i++)
if (String(tasks[i].gid) === gid) {
state.page = Math.floor(i / state.length) + 1
name = state.sortType === "shortName" ? tasks[i].shortName: tasks[i].name
break
}
if(!name)
ElMessage("未找到此任务")
else
ElMessage("已跳转到该任务所在页数,任务名:" + name)
},
_searchLocalByKeyword(state, keyword){
state.searchTask.splice(0)
if(keyword.trim() !== '') {
state.page = 1
let tasks = state.currentTasks
tasks.forEach((task) => {
if (task.name.includes(keyword))
state.searchTask.push(task)
})
if (state.searchTask.length === 0) {
ElMessage("未找到该关键字的任务")
state.isSearch = false
}
else {
state.isSearch = true
}
}else {
state.isSearch = false
confirmCurrentTask(state)
}
},
_deleteGallery(state, gid){
let tasks = [state.totalGalleryTask, state.downloadGallery, state.collectGallery]
deleteTask(tasks, 'gid', gid)
},
_setChosenGallery(state,data){
state.chosenGallery = data.gallery
},
_setCategory(state, category){
state.category = category
confirmCurrentTask(state)
sortTasks(state)
},
_setSortType(state, sortType){
state.sortType = sortType
sortTasks(state)
},
_setGalleryNameType(state, galleryNameType){
state.galleryNameType = galleryNameType
},
_setShowNameType(state, type){
if(type === "shortName")
state.length = state.shortLength
else
state.length = state.defaultLength
},
_setReadingGallery(state, gallery){
if(gallery.images === undefined) {
gallery.images = []
for(let i=1; i<=gallery.pages; i++)
gallery.images.push(buildGalleryManageUrl("/onlineImage/" + i, {
gid: gallery.gid,
AuthCode: state.AuthCode
}));
}
state.readingGallery = gallery
state.isReading = true;
},
_closeReader(state){
state.isReading = false;
},
_openHistoryPanel(state){
state.isShowHistory = true
},
_closeHistoryPanel(state){
state.isShowHistory = false
}
}
const state = {
connectionStatus: "disconnected", // live task updates
websocket: null, //websocket
totalGalleryTask: [], //存放数据的数组
chosenGallery: false, //准备下载
collectGallery: [], //收藏
downloadGallery: [], //下载
isSearch: false, //用于决定是否显示搜索结果
readingGallery: {'name': '', 'images': []}, //在线看
isReading: false, //是否正在看
currentGid: "", //当前GID
lengthPerPage: 0, //在线预览每页图片数量
page: 1, //当前页数
length: 5, //每页能有多少个链接
defaultLength: 4, //默认个数
shortLength: 5, //简洁个数
userId: -1, //用户id
username: "", //用户名
isAuth: false, //是否授权
AuthCode: '', //授权码
loadComplete: false, //是否加载完成
galleryRefreshTimer: 0, //更新计时器id
isInclude: false, //是否搜索到任务
searchTask: [], //搜索到的任务
isShowHistory: false, //是否打开面板
galleryNameType: 'shortName', //名字类型 shortName name
category: 'myDownload', //分类 myDownload myCollect total
sortType:'shortName', //排序类型 shortName name createTime
currentTasks: [], //当前任务
weekUsed: {}, //每周用量
}
const getters = {
currentTasks(state){
if(state.isSearch)
return state.searchTask.slice((state.page - 1) * state.length, state.page * state.length)
else
return state.currentTasks.slice((state.page - 1) * state.length, state.page * state.length)
},
min(){
return 1
},
max(state){
let max = 0
let tasks
if(state.isSearch)
tasks = state.searchTask
else
tasks = state.currentTasks
if(!tasks)
return 1
max = Math.floor(tasks.length/state.length)
if(tasks.length % state.length !== 0 || max === 0)
max += 1
return max
}
}
export default new vuex.Store({
actions,
mutations,
state,
getters
})
function getShortname(name){
if(name === null)
return null
if (!name.includes("[")) return name
// 截取最后一个 [ 之前的部分,然后移除所有 [...] 和 (...) 标签
name = name.substring(0, name.lastIndexOf("["))
return name.replace(/\s*\[[^\]]*\]\s*/g, '').replace(/\s*\([^\)]*\)\s*/g, '').trim()
}
function buildGalleryDownloadUrl(gid, AuthCode){
let params = new URLSearchParams({AuthCode, gid: gid.toString()})
return GalleryManageUrl + "/file/" + gid + ".zip?" + params.toString()
}
function buildGalleryManageUrl(path, params){
return GalleryManageUrl + path + "?" + new URLSearchParams(params).toString()
}
function confirmCurrentTask(state){
switch (state.category){
case 'myDownload':
state.currentTasks = state.downloadGallery
break
case 'myCollect':
state.currentTasks = state.collectGallery
break
case 'total':
state.currentTasks = state.totalGalleryTask
break
}
}
function sortTasks(state){
switch (state.sortType) {
case "name":
state.currentTasks = state.currentTasks.sort((before, after) => {
return before.name > after.name ? 1: -1
})
break
case "shortName":
state.currentTasks = state.currentTasks.sort((before, after) => {
return before.shortName > after.shortName ? 1: -1
})
break
case "createTime":
state.currentTasks = state.currentTasks.sort((before, after) => {
return before.createTime - after.createTime
})
}
}
function deleteTask(tasks, key, value){
for(let j=0; j < tasks.length; j++)
for(let i=0; i<tasks[j].length; i++)
if(tasks[j][i][key] === value){
tasks[j].splice(i, 1)
i--
break
}
}
let status = ['已提交', '下载中', '等待压缩', '压缩中', '下载完成']