增加用户体系

This commit is contained in:
muwoo
2023-07-19 09:43:48 +08:00
parent ca6629988a
commit 47bada5c01
24 changed files with 716 additions and 176 deletions

1
feature/.env.development Normal file
View File

@@ -0,0 +1 @@
VUE_APP_API_BASE=http://localhost:7001/

1
feature/.env.production Normal file
View File

@@ -0,0 +1 @@
VUE_APP_API_BASE=https://rubick.vip/api/

View File

@@ -8,14 +8,15 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@vue/cli-service": "~4.5.0",
"@ant-design/icons-vue": "^6.0.1",
"@vue/cli-service": "~4.5.0",
"ant-design-vue": "3.2.14",
"axios": "^0.24.0",
"core-js": "^3.6.5",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"markdown-it": "^12.2.0",
"nanoid": "^4.0.2",
"vue": "3.2.45",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"

View File

@@ -22,13 +22,7 @@
<template #icon>
<SettingOutlined />
</template>
设置
</a-menu-item>
<a-menu-item key="account">
<template #icon>
<UserOutlined />
</template>
账户
账户与设置
</a-menu-item>
<a-menu-item key="dev">
<template #icon>

View File

@@ -1,7 +1,6 @@
@import "~ant-design-vue/dist/antd.less"; // 引入官方提供的 less 样式入口文件
@primary-color: #ff4ea4; // 全局主色
@link-color: #ff4ea4; // 链接色
@error-color: #ff4ea4; // 错误色
:root {

View File

@@ -0,0 +1,30 @@
import axios from 'axios';
const instance = axios.create({
baseURL: process.env.VUE_APP_API_BASE,
});
export default {
async getScanCode({ scene }: { scene: string }) {
const res = await instance.get('/users/getScanCode', {
params: {
scene,
},
});
return res.data;
},
async checkLoginStatus({ scene }: { scene: string }) {
const res = await instance.post('/users/checkLoginStatus', {
scene,
});
return res.data;
},
async getUserInfo({ openId }: { openId: string }) {
const res = await instance.post('/users/getUserInfo', {
openId,
});
return res.data;
},
};

View File

@@ -1,8 +1,17 @@
import { createApp } from "vue";
import Antd from "ant-design-vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "./assets/ant-reset.less";
import { createApp } from 'vue';
import Antd, { ConfigProvider } from 'ant-design-vue';
import App from './App.vue';
import router from './router';
import store from './store';
import './assets/ant-reset.less';
import 'ant-design-vue/dist/antd.variable.min.css';
createApp(App).use(store).use(Antd).use(router).mount("#app");
const { remote } = window.require('electron');
const { perf } = remote.getGlobal('OP_CONFIG').get();
ConfigProvider.config({
theme: perf.custom || {},
});
createApp(App).use(store).use(Antd).use(router).mount('#app');

View File

@@ -1,39 +1,39 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
import Market from "../views/market/index.vue";
import Installed from "../views/installed/index.vue";
import Account from "../views/account/index.vue";
import Settings from "../views/settings/index.vue";
import Dev from "../views/dev/index.vue";
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import Market from '../views/market/index.vue';
import Installed from '../views/installed/index.vue';
import Account from '../views/account/index.vue';
import Settings from '../views/settings/user.vue';
import Dev from '../views/dev/index.vue';
const routes: Array<RouteRecordRaw> = [
{
path: "/market",
name: "market",
path: '/market',
name: 'market',
component: Market,
},
{
path: "/installed",
name: "installed",
path: '/installed',
name: 'installed',
component: Installed,
},
{
path: "/account",
name: "account",
path: '/account',
name: 'account',
component: Account,
},
{
path: "/settings",
name: "settings",
path: '/settings',
name: 'settings',
component: Settings,
},
{
path: "/dev",
name: "dev",
path: '/dev',
name: 'dev',
component: Dev,
},
{
path: "/:catchAll(.*)",
name: "market",
path: '/:catchAll(.*)',
name: 'market',
component: Market,
},
];

View File

@@ -1,13 +1,72 @@
<template>
<div class="account">
<a-result status="404" title="玩命开发中" sub-title="个人中心正在开发中敬请期待...">
<a-result
v-if="!userInfo"
title="请先登录"
sub-title="用户暂未登录无法体验更多设置"
>
<template #extra>
<a-button @click="showModal" type="primary">
使用微信小程序登录
</a-button>
</template>
</a-result>
<a-modal :footer="null" v-model:visible="visible">
<a-result
title="请使用微信扫码登录!"
sub-title="使用微信扫描上面的 rubick 小程序二维码进行授权登录"
>
<template #icon>
<img width="200" :src="imgCode" />
</template>
</a-result>
</a-modal>
</div>
</template>
<script>
export default {
<script setup>
import { nanoid } from 'nanoid';
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import service from '../../assets/service';
const userInfo = ref(window.rubick.dbStorage.getItem('rubick-user-info'));
const imgCode = ref('');
const scene = nanoid();
const visible = ref(false);
const showModal = () => {
visible.value = true;
if (!imgCode.value && !userInfo.value) {
service.getScanCode({ scene }).then(res => {
imgCode.value = `data:image/png;base64,${res.dataUrl}`;
});
}
};
let timer = null;
watch([visible], () => {
if (visible.value) {
timer = setInterval(() => {
service.checkLoginStatus({ scene }).then((res) => {
console.log(res);
if (res.openId) {
window.rubick.dbStorage.setItem('rubick-user-info', res);
userInfo.value = res;
message.success('登录成功!');
visible.value = false;
clearInterval(timer);
timer = null;
}
});
}, 2000);
} else {
clearInterval(timer);
timer = null;
}
});
</script>
<style lang="less" scoped>

View File

@@ -64,7 +64,6 @@
:key="cmd"
v-for="cmd in item.cmds"
:class="{ executable: !cmd.label }"
:color="!cmd.label && '#87d068'"
>
<span
@click="
@@ -329,7 +328,7 @@ const deletePlugin = async (plugin) => {
&.executable {
cursor: pointer;
color: var(--ant-info-color);
&:hover {
transform: translateY(-2px);
}

View File

@@ -7,7 +7,7 @@
<a-list-item v-if="item" @click="showDetail(item)">
<template #actions>
<a-button
style="color: #ff4ea4"
class="download-plugin-btn"
type="text"
:loading="item.isloading"
>
@@ -134,6 +134,9 @@ const showDetail = async item => {
}
.panel-item {
margin: 20px 0;
.download-plugin-btn {
color: var(--ant-primary-color);
}
.title {
margin-bottom: 30px;
color: var(--color-text-primary);

View File

@@ -2,27 +2,33 @@
<div class="settings">
<div class="left-menu">
<a-menu v-model:selectedKeys="currentSelect" mode="inline">
<a-menu-item key="userInfo">
<template #icon>
<UserOutlined/>
</template>
账户信息
</a-menu-item>
<a-menu-item key="normal">
<template #icon>
<ToolOutlined />
<ToolOutlined/>
</template>
基本设置
</a-menu-item>
<a-menu-item key="global">
<template #icon>
<LaptopOutlined />
<LaptopOutlined/>
</template>
全局快捷键
</a-menu-item>
<a-menu-item key="superpanel">
<template #icon>
<FileAddOutlined />
<FileAddOutlined/>
</template>
超级面板设置
</a-menu-item>
<a-menu-item key="localhost">
<template #icon>
<DatabaseOutlined />
<DatabaseOutlined/>
</template>
内网部署配置
</a-menu-item>
@@ -38,7 +44,7 @@
<template #title>
<span>{{ tipText }}</span>
<template v-if="isWindows">
<br />
<br/>
<span
style="cursor: pointer; text-decoration: underline"
@click="resetDefault('Alt')"
@@ -128,7 +134,7 @@
按下快捷键自动搜索对应关键字当关键字结果完全匹配且结果唯一时会直接指向该功能
</div>
<h3 style="margin-top: 10px">示例</h3>
<a-divider style="margin: 5px 0" />
<a-divider style="margin: 5px 0"/>
<a-list item-layout="horizontal" :data-source="examples">
<template #renderItem="{ item }">
<a-list-item>
@@ -177,12 +183,13 @@
</div>
</div>
<div @click="addConfig" class="add-global">
<PlusCircleOutlined />
<PlusCircleOutlined/>
新增全局快捷功能
</div>
</div>
<Localhost v-if="currentSelect[0] === 'localhost'" />
<SuperPanel v-if="currentSelect[0] === 'superpanel'" />
<Localhost v-if="currentSelect[0] === 'localhost'"/>
<SuperPanel v-if="currentSelect[0] === 'superpanel'"/>
<UserInfo v-if="currentSelect[0] === 'userInfo'"/>
</div>
</div>
</template>
@@ -195,14 +202,16 @@ import {
MinusCircleOutlined,
PlusCircleOutlined,
FileAddOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import debounce from 'lodash.debounce';
import { ref, reactive, watch, toRefs, computed, toRaw } from 'vue';
import {ref, reactive, watch, toRefs, computed, toRaw} from 'vue';
import keycodes from './keycode';
import Localhost from './localhost.vue';
import SuperPanel from './super-panel.vue';
import UserInfo from './user-info';
const { remote, ipcRenderer } = window.require('electron');
const {remote, ipcRenderer} = window.require('electron');
const examples = [
{
@@ -224,6 +233,7 @@ const state = reactive({
common: {},
local: {},
global: [],
custom: {},
});
const isWindows = window?.rubick?.isWindows();
@@ -232,11 +242,12 @@ const tipText = computed(() => {
return `先按功能键Ctrl、Shift、${optionKeyName}),再按其他普通键。`;
});
const currentSelect = ref(['normal']);
const currentSelect = ref(['userInfo']);
const { perf, global: defaultGlobal } = remote.getGlobal('OP_CONFIG').get();
const {perf, global: defaultGlobal} = remote.getGlobal('OP_CONFIG').get();
state.shortCut = perf.shortCut;
state.custom = perf.custom;
state.common = perf.common;
state.local = perf.local;
state.global = defaultGlobal;
@@ -249,6 +260,7 @@ const setConfig = debounce(() => {
shortCut: state.shortCut,
common: state.common,
local: state.local,
custom: state.custom,
},
global: state.global,
})
@@ -279,11 +291,11 @@ const changeShortCut = (e, key) => {
compose += '+Command';
incluFuncKeys = true;
}
compose += '+'+keycodes[e.keyCode].toUpperCase();
compose += '+' + keycodes[e.keyCode].toUpperCase();
compose = compose.substring(1)
if(incluFuncKeys && e.keyCode !== 16 && e.keyCode !== 17 && e.keyCode !== 18 && e.keyCode !== 93){
if (incluFuncKeys && e.keyCode !== 16 && e.keyCode !== 17 && e.keyCode !== 18 && e.keyCode !== 93) {
state.shortCut[key] = compose;
}else{
} else {
// 不做处理
}
};
@@ -354,11 +366,12 @@ const addConfig = () => {
});
};
const { shortCut, common, local, global } = toRefs(state);
const {shortCut, common, local, global} = toRefs(state);
</script>
<style lang="less" scoped>
<style lang="less">
@import '~@/assets/common.less';
.settings {
box-sizing: border-box;
width: 100%;
@@ -366,6 +379,12 @@ const { shortCut, common, local, global } = toRefs(state);
background: var(--color-body-bg);
height: calc(~'100vh - 46px');
display: flex;
.ant-menu {
background: var(--color-body-bg) !important;
color: var(--color-text-content) !important;
}
.settings-detail {
padding: 20px;
box-sizing: border-box;
@@ -373,16 +392,20 @@ const { shortCut, common, local, global } = toRefs(state);
overflow: auto;
height: 100%;
background: var(--color-body-bg);
.setting-item {
margin-bottom: 20px;
.ant-form-item {
margin-bottom: 0;
}
.title {
color: #6c9fe2;
color: var(--ant-primary-color);
font-size: 15px;
margin-bottom: 10px;
}
.settings-item-li {
padding-left: 20px;
display: flex;
@@ -390,20 +413,30 @@ const { shortCut, common, local, global } = toRefs(state);
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
.label {
color: var(--color-text-content);
}
.value {
width: 300px;
cursor: pointer;
text-align: center;
border: 1px solid var(--color-border-light);
color: #6c9fe2;
color: var(--ant-primary-color);
font-size: 14px;
height: 24px;
font-weight: lighter;
.ant-input {
text-align: center;
color: var(--ant-primary-color);
font-size: 14px;
font-weight: lighter;
}
}
:deep(.ant-switch) {
.ant-switch {
&:not(.ant-switch-checked) {
background: var(--color-list-hover);
}
@@ -411,48 +444,56 @@ const { shortCut, common, local, global } = toRefs(state);
}
}
}
.feature-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
font-size: 14px;
.item {
flex: 1;
color: var(--color-text-content);
}
.short-cut {
margin-left: 20px;
}
.value {
cursor: pointer;
text-align: center;
border: 1px solid var(--color-border-light);
color: #6c9fe2;
color: var(--ant-primary-color);
font-size: 14px;
height: 24px;
font-weight: lighter;
margin-top: 10px;
position: relative;
background: var(--color-input-hover);
:deep(.ant-input) {
color: #6c9fe2;
.ant-input {
color: var(--ant-primary-color);
font-weight: lighter;
background: none;
}
:deep(.anticon) {
.anticon {
color: var(--color-text-desc);
}
&.ant-input-affix-wrapper {
display: flex;
}
&:hover {
.anticon {
display: block;
color: var(--color-text-content);
}
}
.anticon {
position: absolute;
display: none;
@@ -462,24 +503,29 @@ const { shortCut, common, local, global } = toRefs(state);
}
}
}
.add-global {
color: #6c9fe2;
color: var(--ant-primary-color);
margin-top: 20px;
width: 100%;
text-align: center;
cursor: pointer;
}
:deep(.ant-collapse) {
.ant-collapse {
background: var(--color-input-hover);
.ant-collapse-content {
background: var(--color-input-hover);
color: var(--color-text-content);
}
h3,
.ant-collapse-header,
.ant-list-item-meta-title {
color: var(--color-text-primary);
}
.ant-list-item-meta-description {
color: var(--color-text-desc);
}

View File

@@ -0,0 +1,178 @@
<template>
<div class="user-info">
<div class="info-container">
<a-result
class="user-info-result"
:title="userInfo.name || 'rubick 用户'"
sub-title="软件偏好设置完成后需重启软件头像和昵称请前往小程序设置"
>
<template #icon>
<a-avatar :size="64" v-if="!userInfo.avatar">
<template #icon><UserOutlined /></template>
</a-avatar>
<a-avatar :src="userInfo.avatar" :size="64" v-else />
</template>
</a-result>
</div>
<div class="settings-container">
<div class="setting-item">
<div class="title">主题色设置</div>
<div class="settings-item-li">
<div class="label">主色调</div>
<a-input v-model:value="custom.primaryColor" class="value">
<template #prefix>
<div :style="{ background: custom.primaryColor, width: '10px', height: '10px' }"></div>
</template>
</a-input>
</div>
<div class="settings-item-li">
<div class="label">错误色</div>
<a-input v-model:value="custom.errorColor" class="value">
<template #prefix>
<div :style="{ background: custom.errorColor, width: '10px', height: '10px' }"></div>
</template>
</a-input>
</div>
<div class="settings-item-li">
<div class="label">警告色</div>
<a-input v-model:value="custom.warningColor" class="value">
<template #prefix>
<div :style="{ background: custom.warningColor, width: '10px', height: '10px' }"></div>
</template>
</a-input>
</div>
<div class="settings-item-li">
<div class="label">成功色</div>
<a-input v-model:value="custom.successColor" class="value">
<template #prefix>
<div :style="{ background: custom.successColor, width: '10px', height: '10px' }"></div>
</template>
</a-input>
</div>
<div class="settings-item-li">
<div class="label">提醒色</div>
<a-input v-model:value="custom.infoColor" class="value">
<template #prefix>
<div :style="{ background: custom.infoColor, width: '10px', height: '10px' }"></div>
</template>
</a-input>
</div>
</div>
<div class="setting-item">
<div class="title">用户个性化设置</div>
<div class="settings-item-li">
<div class="label">主搜索框欢迎语</div>
<a-input v-model:value="custom.placeholder" class="value"></a-input>
</div>
<div class="settings-item-li">
<div class="label">界面 logo</div>
<div class="img-container">
<img
class="custom-img"
:src="custom.logo"
/>
<a-button @click="changeLogo" size="small" type="link">点我替换</a-button>
</div>
</div>
</div>
<div class="footer-btn">
<a-button @click="reset" type="danger">恢复默认设置</a-button>
</div>
</div>
</div>
</template>
<script setup>
import {reactive, ref, toRefs, watch} from 'vue';
import { Modal } from 'ant-design-vue';
import { UserOutlined } from '@ant-design/icons-vue';
import debounce from 'lodash.debounce';
import service from '../../assets/service';
const { remote, ipcRenderer } = window.require('electron');
const state = reactive({
custom: {},
});
const { perf } = remote.getGlobal('OP_CONFIG').get();
state.custom = perf.custom || {};
const userInfo = ref(window.rubick.dbStorage.getItem('rubick-user-info'));
service.getUserInfo({ openId: userInfo.value.openId }).then((res) => {
userInfo.value = res;
});
const setConfig = debounce(() => {
remote.getGlobal('OP_CONFIG').set(
JSON.parse(
JSON.stringify({
perf: {
...perf,
custom: state.custom,
},
})
)
);
ipcRenderer.send('re-register');
}, 500);
watch(state, setConfig);
const { custom } = toRefs(state);
const changeLogo = () => {
const [logoPath] = window.rubick.showOpenDialog({
title: '请选择 logo 路径',
filters: [{ name: 'images', extensions: ['png'] }],
properties: ['openFile'],
});
state.custom.logo = `file://${logoPath}`;
};
const reset = () => {
Modal.warning({
title: '确定恢复默认设置吗?',
content: '回复后之前的设置将会被清空',
onOk() {
const defaultcustom = remote.getGlobal('OP_CONFIG').getDefaultConfig().perf.custom;
state.custom = JSON.parse(JSON.stringify(defaultcustom));
},
});
};
</script>
<style lang="less">
.settings-container {
margin-top: 18px;
}
.user-info-result {
padding: 0;
&.ant-result {
padding: 24px;
}
.icon {
font-size: 48px;
}
.ant-result-icon {
margin-bottom: 12px;
}
.ant-result-title {
font-size: 18px;
}
}
.img-container {
width: 300px;
}
.custom-img {
width: 60px;
height: 60px;
}
.footer-btn {
text-align: right;
border-top: 1px dashed #ddd;
padding-top: 12px;
}
</style>

View File

@@ -0,0 +1,88 @@
<template>
<div class="account">
<a-result
v-if="!userInfo"
title="请先登录"
sub-title="登录后可开启用户个性化设置"
>
<template #extra>
<a-button @click="showModal" type="primary">
使用微信小程序登录
</a-button>
</template>
</a-result>
<Index v-else />
<a-modal :footer="null" v-model:visible="visible">
<a-result
title="请使用微信扫码登录!"
sub-title="使用微信扫描上面的 rubick 小程序二维码进行授权登录"
>
<template #icon>
<img width="200" :src="imgCode" />
</template>
</a-result>
</a-modal>
</div>
</template>
<script setup>
import { nanoid } from 'nanoid';
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import Index from './index';
import service from '../../assets/service';
const userInfo = ref(window.rubick.dbStorage.getItem('rubick-user-info'));
const imgCode = ref('');
const scene = nanoid();
const visible = ref(false);
const showModal = () => {
visible.value = true;
if (!imgCode.value && !userInfo.value) {
service.getScanCode({ scene }).then(res => {
imgCode.value = `data:image/png;base64,${res.dataUrl}`;
});
}
};
let timer = null;
watch([visible], () => {
if (visible.value) {
timer = setInterval(() => {
service.checkLoginStatus({ scene }).then((res) => {
console.log(res);
if (res.openId) {
window.rubick.dbStorage.setItem('rubick-user-info', res);
userInfo.value = res;
message.success('登录成功!');
visible.value = false;
clearInterval(timer);
timer = null;
}
});
}, 2000);
} else {
clearInterval(timer);
timer = null;
}
});
</script>
<style lang="less" scoped>
.account {
box-sizing: border-box;
width: 100%;
overflow-x: hidden;
background: var(--color-body-bg);
height: calc(~"100vh - 46px");
:deep(.ant-result-title) {
color: var(--color-text-primary);
}
:deep(.ant-result-subtitle) {
color: var(--color-text-desc);
}
}
</style>

View File

@@ -6408,6 +6408,11 @@ nanoid@^3.3.4:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
nanoid@^4.0.2:
version "4.0.2"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e"
integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -9112,6 +9117,11 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0:
version "9.0.0"
resolved "https://registry.npmmirror.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
v8-compile-cache@^2.0.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"