mirror of
https://github.com/CzBiX/qb-web.git
synced 2026-04-14 02:10:31 +08:00
Add remote QB support
This commit is contained in:
@@ -12,9 +12,8 @@ Languages: English, 中文
|
||||
|
||||
[TODO](https://github.com/CzBiX/qb-web/projects/2)
|
||||
|
||||
## Download
|
||||
|
||||
[Releases](https://github.com/CzBiX/qb-web/releases/latest)
|
||||
## How to use
|
||||
see: [Wiki](https://github.com/CzBiX/qb-web/wiki/How-to-use)
|
||||
|
||||
## Install
|
||||
|
||||
|
||||
BIN
screenshot/CORS-settings.png
Normal file
BIN
screenshot/CORS-settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
28
src/Api.ts
28
src/Api.ts
@@ -2,18 +2,36 @@
|
||||
import Axios, { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { RssNode, RssRule, SearchPlugin, ApiCategory, SearchTaskResponse } from '@/types';
|
||||
|
||||
class Api {
|
||||
const apiEndpoint = 'api/v2';
|
||||
|
||||
class Api {
|
||||
private axios: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.axios = Axios.create({
|
||||
baseURL: 'api/v2',
|
||||
baseURL: apiEndpoint,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
this.axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
|
||||
private normalizeBaseUrl(baseUrl?: string) {
|
||||
if (!baseUrl) {
|
||||
return apiEndpoint;
|
||||
}
|
||||
|
||||
if (!baseUrl.endsWith('/')) {
|
||||
baseUrl += '/';
|
||||
}
|
||||
|
||||
return baseUrl + apiEndpoint;
|
||||
}
|
||||
|
||||
public changeBaseUrl(baseUrl: string) {
|
||||
this.axios.defaults.baseURL = this.normalizeBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
public getAppVersion() {
|
||||
return this.axios.get('/app/version');
|
||||
}
|
||||
@@ -22,12 +40,13 @@ class Api {
|
||||
return this.axios.get('/app/webapiVersion');
|
||||
}
|
||||
|
||||
public login(params: any) {
|
||||
public login(params: any, baseUrl?: string) {
|
||||
const data = new URLSearchParams(params);
|
||||
return this.axios.post('/auth/login', data, {
|
||||
validateStatus(status) {
|
||||
return status === 200 || status === 403;
|
||||
},
|
||||
baseURL: this.normalizeBaseUrl(baseUrl),
|
||||
}).then(Api.handleResponse);
|
||||
}
|
||||
|
||||
@@ -330,4 +349,5 @@ class Api {
|
||||
}
|
||||
}
|
||||
|
||||
export default new Api();
|
||||
const api = new Api();
|
||||
export default api;
|
||||
|
||||
26
src/App.vue
26
src/App.vue
@@ -75,6 +75,8 @@ import api from './Api';
|
||||
import Component from 'vue-class-component';
|
||||
import { Watch } from 'vue-property-decorator';
|
||||
import { MainData } from './types';
|
||||
import { Config } from './store/config';
|
||||
import Api from './Api';
|
||||
|
||||
let appWrapEl: HTMLElement;
|
||||
|
||||
@@ -98,6 +100,7 @@ let appWrapEl: HTMLElement;
|
||||
'mainData',
|
||||
'rid',
|
||||
'preferences',
|
||||
'needAuth',
|
||||
]),
|
||||
...mapGetters(['config']),
|
||||
},
|
||||
@@ -106,11 +109,11 @@ let appWrapEl: HTMLElement;
|
||||
'updateMainData',
|
||||
'updatePreferences',
|
||||
'setPasteUrl',
|
||||
'updateNeedAuth',
|
||||
]),
|
||||
}
|
||||
})
|
||||
export default class App extends Vue {
|
||||
needAuth = false
|
||||
drawer = true
|
||||
drawerOptions = {
|
||||
showLogs: false,
|
||||
@@ -122,11 +125,13 @@ export default class App extends Vue {
|
||||
mainData!: MainData
|
||||
rid!: number
|
||||
preferences!: any
|
||||
config!: any
|
||||
config!: Config
|
||||
needAuth!: boolean
|
||||
|
||||
updateMainData!: (_: any) => void
|
||||
updatePreferences!: (_: any) => void
|
||||
setPasteUrl!: (_: any) => void
|
||||
updateNeedAuth!: (_: boolean) => void
|
||||
|
||||
get phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
@@ -159,13 +164,22 @@ export default class App extends Vue {
|
||||
}
|
||||
|
||||
async getInitData() {
|
||||
const href = location.href;
|
||||
if (!this.config.baseUrl) {
|
||||
if (href.includes("czbix.github.io") || href.includes("localhost")) {
|
||||
this.updateNeedAuth(true);
|
||||
return;
|
||||
} else {
|
||||
Api.changeBaseUrl(href);
|
||||
}
|
||||
} else {
|
||||
Api.changeBaseUrl(this.config.baseUrl);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.getMainData();
|
||||
} catch (e) {
|
||||
if (e.response.status === 403) {
|
||||
this.needAuth = true;
|
||||
}
|
||||
|
||||
this.updateNeedAuth(true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,17 @@
|
||||
@keyup.enter.capture="submit"
|
||||
v-bind="{ [`grid-list-${$vuetify.breakpoint.name}`]: true }"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="baseUrl"
|
||||
prepend-icon="mdi-network"
|
||||
:label="$t('label.base_url')"
|
||||
autofocus
|
||||
required
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="params.username"
|
||||
prepend-icon="mdi-account"
|
||||
:label="$t('username')"
|
||||
:rules="[v => !!v || $t('msg.item_is_required', { item: $t('username') })]"
|
||||
autofocus
|
||||
required
|
||||
/>
|
||||
@@ -36,7 +42,6 @@
|
||||
@click:append="showPassword = !showPassword"
|
||||
:label="$t('password')"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:rules="[v => !!v || $t('msg.item_is_required', { item: $t('password') })]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -63,56 +68,64 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent, reactive, toRefs } from '@vue/composition-api';
|
||||
|
||||
import { tr } from '@/locale';
|
||||
import api from '../Api';
|
||||
import api from '@/Api';
|
||||
import { useStore } from '@/store';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tr,
|
||||
setup(props, { emit }) {
|
||||
const data = reactive({
|
||||
valid: false,
|
||||
submitting: false,
|
||||
showPassword: false,
|
||||
loginError: null,
|
||||
baseUrl: location.href,
|
||||
params: {
|
||||
username: null,
|
||||
password: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
form: null,
|
||||
});
|
||||
const store = useStore();
|
||||
|
||||
methods: {
|
||||
async submit() {
|
||||
if (this.submitting) {
|
||||
const submit = async () => {
|
||||
if (data.submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(this.$refs.form as any).validate()) {
|
||||
if (!(data.form as any).validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitting = true;
|
||||
let data;
|
||||
data.submitting = true;
|
||||
try {
|
||||
data = await api.login(this.params);
|
||||
const resp = await api.login(data.params, data.baseUrl);
|
||||
|
||||
if (data === 'Ok.') {
|
||||
this.$emit('input', false);
|
||||
if (resp === 'Ok.') {
|
||||
store.commit('updateConfig', {
|
||||
key: 'baseUrl',
|
||||
value: data.baseUrl,
|
||||
});
|
||||
emit('input', false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.loginError = data;
|
||||
data.loginError = resp;
|
||||
} catch (e) {
|
||||
this.loginError = e.message;
|
||||
data.loginError = e.message;
|
||||
}
|
||||
|
||||
this.submitting = false;
|
||||
},
|
||||
data.submitting = false;
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(data),
|
||||
submit,
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -99,6 +99,7 @@ export default {
|
||||
reannounced: 'Reannounced',
|
||||
rechecking: 'Rechecking…',
|
||||
dht_nodes: '%{smart_count} node |||| %{smart_count} nodes',
|
||||
base_url: 'Base URL',
|
||||
},
|
||||
|
||||
msg: {
|
||||
|
||||
@@ -30,7 +30,7 @@ function matchLocale() {
|
||||
export const defaultLocale = matchLocale()
|
||||
|
||||
function updateLocale() {
|
||||
let locale: LocaleKey | undefined = loadConfig()['locale'];
|
||||
let locale = loadConfig()['locale'] as LocaleKey;
|
||||
|
||||
if (!locale) {
|
||||
locale = defaultLocale;
|
||||
|
||||
@@ -5,7 +5,22 @@ import { ConfigState, ConfigPayload } from './types';
|
||||
|
||||
const configKey = 'qb-config';
|
||||
|
||||
export interface Config {
|
||||
baseUrl: string | null;
|
||||
updateInterval: number;
|
||||
pageOptions: any;
|
||||
filter: {
|
||||
state: string | null;
|
||||
category: string | null;
|
||||
site: string | null;
|
||||
query: string | null;
|
||||
};
|
||||
locale: string | null;
|
||||
darkMode: string | null;
|
||||
}
|
||||
|
||||
const defaultConfig = {
|
||||
baseUrl: null,
|
||||
updateInterval: 2000,
|
||||
pageOptions: {
|
||||
itemsPerPage: 50,
|
||||
@@ -20,8 +35,6 @@ const defaultConfig = {
|
||||
darkMode: null,
|
||||
};
|
||||
|
||||
export type Config = typeof defaultConfig
|
||||
|
||||
function saveConfig(obj: any) {
|
||||
localStorage.setItem(configKey, JSON.stringify(obj));
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const store = new Vuex.Store<RootState>({
|
||||
mainData: undefined,
|
||||
preferences: null,
|
||||
pasteUrl: null,
|
||||
needAuth: false,
|
||||
},
|
||||
mutations: {
|
||||
/* eslint-disable no-param-reassign */
|
||||
@@ -60,6 +61,9 @@ const store = new Vuex.Store<RootState>({
|
||||
const { url } = payload;
|
||||
state.pasteUrl = url;
|
||||
},
|
||||
updateNeedAuth(state, payload) {
|
||||
state.needAuth = payload;
|
||||
},
|
||||
/* eslint-enable no-param-reassign */
|
||||
},
|
||||
getters: {
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface RootState {
|
||||
mainData?: MainData;
|
||||
preferences: any;
|
||||
pasteUrl: string | null;
|
||||
needAuth: boolean;
|
||||
}
|
||||
|
||||
export interface SearchEnginePage {
|
||||
|
||||
@@ -13,6 +13,7 @@ const emtpyState: RootState = {
|
||||
mainData: undefined,
|
||||
preferences: null,
|
||||
pasteUrl: null,
|
||||
needAuth: false,
|
||||
};
|
||||
|
||||
const mockState = mock(emtpyState);
|
||||
|
||||
Reference in New Issue
Block a user