增加移动端登录页面

This commit is contained in:
RockYang
2024-12-26 16:52:20 +08:00
parent acee2d9d81
commit 8af0fec8ec
25 changed files with 831 additions and 1012 deletions

View File

@@ -9,8 +9,10 @@
</div>
<div class="flex" :class="{ 'top-collapse': !isCollapse }">
<div class="top-avatar flex">
<span class="title" v-if="!isCollapse">GeekAI</span>
<img v-if="loginUser.id" :src="!!loginUser.avatar ? loginUser.avatar : avatarImg" alt="" :class="{ marr: !isCollapse }" />
<span class="title" v-if="!isCollapse">{{ title }}</span>
<el-tooltip :content="title" placement="right">
<img :src="logo" alt="" :class="{ marr: !isCollapse }" v-if="isCollapse" />
</el-tooltip>
</div>
<div class="menuIcon xxx" @click="isCollapse = !isCollapse">
<el-tooltip content="关闭菜单" placement="right" v-if="!isCollapse">
@@ -80,6 +82,9 @@
</template>
<template #default>
<ul class="more-menus setting-menus">
<li>
<img :src="loginUser.avatar ? loginUser.avatar : avatarImg" />
</li>
<li>
<div @click="showConfigDialog = true" class="flex">
<el-icon>
@@ -113,12 +118,6 @@
</div>
</div>
<el-scrollbar class="right-main">
<div
v-if="loginUser.id === undefined || !loginUser.id"
class="loginMask"
:style="{ left: isCollapse ? '65px' : '170px' }"
@click="showNoticeLogin = true"
></div>
<div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
<el-button @click="router.push('/login')" class="btn-go animate__animated animate__pulse animate__infinite" round>登录</el-button>
</div>
@@ -132,12 +131,14 @@
<!-- </div> -->
</el-scrollbar>
<config-dialog v-if="loginUser.id" :show="showConfigDialog" @hide="showConfigDialog = false" />
<el-dialog v-model="showNoticeLogin">
<el-result icon="warning" title="未登录" sub-title="登录后解锁功能">
<template #extra>
<el-button type="primary" @click="router.push('/login')">登录</el-button>
</template>
</el-result>
<el-dialog v-model="showLoginDialog" width="500px" @close="store.setShowLoginDialog(false)">
<template #header>
<div class="text-center text-xl" style="color: var(--theme-text-color-primary)">登录后解锁功能</div>
</template>
<div class="p-4 pt-2 pb-2">
<LoginDialog @success="loginSuccess" @hide="store.setShowLoginDialog(false)" />
</div>
</el-dialog>
</div>
</template>
@@ -145,7 +146,6 @@
<script setup>
import { CirclePlus, Setting, UserFilled } from "@element-plus/icons-vue";
import ThemeChange from "@/components/ThemeChange.vue";
import avatarImg from "@/assets/img/avatar.jpg";
import { useRouter } from "vue-router";
import { onMounted, ref, watch } from "vue";
import { httpGet } from "@/utils/http";
@@ -155,6 +155,8 @@ import { removeUserToken } from "@/store/session";
import { useSharedStore } from "@/store/sharedata";
import ConfigDialog from "@/components/UserInfoDialog.vue";
import { showMessageError } from "@/utils/dialog";
import LoginDialog from "@/components/LoginDialog.vue";
import { substr } from "@/utils/libs";
const isCollapse = ref(true);
const router = useRouter();
@@ -164,7 +166,14 @@ const moreNavs = ref([]);
const curPath = ref();
const title = ref("");
const showNoticeLogin = ref(false);
const avatarImg = ref("/images/avatar/default.jpg");
const store = useSharedStore();
const loginUser = ref({});
const routerViewKey = ref(0);
const showConfigDialog = ref(false);
const license = ref({ de_copy: true });
const gitURL = ref(process.env.VUE_APP_GIT_URL);
const showLoginDialog = ref(false);
/**
* 从路径名中提取第一个路径段
@@ -190,18 +199,11 @@ const getFirstPathSegment = (url) => {
return null;
}
};
const loginUser = ref({});
const routerViewKey = ref(0);
const showConfigDialog = ref(false);
const license = ref({ de_copy: true });
const gitURL = ref(process.env.VUE_APP_GIT_URL);
const store = useSharedStore();
const show = ref(false);
watch(
() => store.showLoginDialog,
(newValue) => {
show.value = newValue;
showLoginDialog.value = newValue;
}
);
@@ -216,7 +218,6 @@ if (curPath.value === "/external") {
}
const changeNav = (item) => {
curPath.value = item.url;
console.log(item.url);
if (item.url.indexOf("http") !== -1) {
// 外部链接
router.push({ path: "/external", query: { url: item.url, title: item.name } });
@@ -280,16 +281,19 @@ const logout = function () {
httpGet("/api/user/logout")
.then(() => {
removeUserToken();
store.setShowLoginDialog(true);
store.setIsLogin(false);
loginUser.value = {};
// 刷新组件
routerViewKey.value += 1;
router.push("/login");
})
.catch(() => {
ElMessage.error("注销失败!");
});
};
const loginSuccess = () => {
init();
store.setShowLoginDialog(false);
// 刷新组件
routerViewKey.value += 1;
};
</script>
<style lang="stylus" scoped>

View File

@@ -311,110 +311,7 @@
</div>
<!-- 任务详情弹框 -->
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true">
<el-row :gutter="20">
<el-col :span="16">
<div class="img-container" :style="{ maxHeight: fullImgHeight + 'px' }">
<el-image :src="item['img_url']" fit="contain" />
</div>
</el-col>
<el-col :span="8">
<div class="task-info">
<div class="info-line">
<el-divider> 正向提示词 </el-divider>
<div class="prompt">
<span>{{ item.prompt }}</span>
<el-icon class="copy-prompt-sd" :data-clipboard-text="item.prompt">
<DocumentCopy />
</el-icon>
</div>
</div>
<div class="info-line">
<el-divider> 反向提示词 </el-divider>
<div class="prompt">
<span>{{ item.params.neg_prompt }}</span>
<el-icon class="copy-prompt-sd" :data-clipboard-text="item.params.neg_prompt">
<DocumentCopy />
</el-icon>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>采样方法</label>
<div class="item-value">{{ item.params.sampler }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>图片尺寸</label>
<div class="item-value">{{ item.params.width }} x {{ item.params.height }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>迭代步数</label>
<div class="item-value">{{ item.params.steps }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>引导系数</label>
<div class="item-value">{{ item.params.cfg_scale }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>随机因子</label>
<div class="item-value">{{ item.params.seed }}</div>
</div>
</div>
<div v-if="item.params.hd_fix">
<el-divider> 高清修复 </el-divider>
<div class="info-line">
<div class="wrapper">
<label>重绘幅度</label>
<div class="item-value">
{{ item.params.hd_redraw_rate }}
</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>放大算法</label>
<div class="item-value">{{ item.params.hd_scale_alg }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>放大倍数</label>
<div class="item-value">{{ item.params.hd_scale }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>迭代步数</label>
<div class="item-value">{{ item.params.hd_steps }}</div>
</div>
</div>
</div>
<div class="copy-params">
<el-button type="primary" round @click="copyParams(item)">画一张同款的</el-button>
</div>
</div>
</el-col>
</el-row>
</el-dialog>
<sd-task-view v-model="showTaskDialog" :data="item" @drawSame="copyParams" @close="showTaskDialog = false" />
</div>
</div>
</template>
@@ -434,7 +331,7 @@ import { useSharedStore } from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue";
import { showMessageError } from "@/utils/dialog";
import SdTaskView from "@/components/SdTaskView.vue";
const listBoxHeight = ref(0);
// const paramBoxHeight = ref(0)
const fullImgHeight = ref(window.innerHeight - 60);

View File

@@ -3,7 +3,7 @@
<div class="inner custom-scroll">
<div class="header">
<h2 class="text-xl pt-4 pb-4">AI 绘画作品墙</h2>
<div class="settings">
<div class="settings pr-14">
<el-radio-group v-model="imgType" @change="changeImgType">
<el-radio value="mj" size="large">MidJourney</el-radio>
<el-radio value="sd" size="large">Stable Diffusion</el-radio>
@@ -161,120 +161,7 @@
<!-- end of waterfall -->
</div>
<!-- 任务详情弹框 -->
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true">
<el-row :gutter="20">
<el-col :span="16">
<div class="img-container" :style="{ maxHeight: fullImgHeight + 'px' }">
<el-image :src="item['img_url']" fit="contain">
<template #placeholder>
<div class="image-slot">正在加载图片</div>
</template>
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
</div>
</el-col>
<el-col :span="8">
<div class="task-info">
<div class="info-line">
<el-divider> 正向提示词 </el-divider>
<div class="prompt">
<span>{{ item.prompt }}</span>
<el-icon class="copy-prompt-wall" :data-clipboard-text="item.prompt">
<DocumentCopy />
</el-icon>
</div>
</div>
<div class="info-line">
<el-divider> 反向提示词 </el-divider>
<div class="prompt">
<span>{{ item.params.negative_prompt }}</span>
<el-icon class="copy-prompt-wall" :data-clipboard-text="item.params.negative_prompt">
<DocumentCopy />
</el-icon>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>采样方法</label>
<div class="item-value">{{ item.params.sampler }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>图片尺寸</label>
<div class="item-value">{{ item.params.width }} x {{ item.params.height }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>迭代步数</label>
<div class="item-value">{{ item.params.steps }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>引导系数</label>
<div class="item-value">{{ item.params.cfg_scale }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>随机因子</label>
<div class="item-value">{{ item.params.seed }}</div>
</div>
</div>
<div v-if="item.params.hd_fix">
<el-divider> 高清修复 </el-divider>
<div class="info-line">
<div class="wrapper">
<label>重绘幅度</label>
<div class="item-value">{{ item.params.hd_redraw_rate }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>放大算法</label>
<div class="item-value">{{ item.params.hd_scale_alg }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>放大倍数</label>
<div class="item-value">{{ item.params.hd_scale }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>迭代步数</label>
<div class="item-value">{{ item.params.hd_steps }}</div>
</div>
</div>
</div>
<div class="copy-params">
<el-button type="primary" round @click="drawSameSd(item)">画一张同款的</el-button>
</div>
</div>
</el-col>
</el-row>
</el-dialog>
<sd-task-view v-model="showTaskDialog" :data="item" @drawSame="drawSameSd" @close="showTaskDialog = false" />
</div>
</template>
@@ -287,7 +174,7 @@ import { ElMessage } from "element-plus";
import Clipboard from "clipboard";
import { useRouter } from "vue-router";
import BackTop from "@/components/BackTop.vue";
import SdTaskView from "@/components/SdTaskView.vue";
const data = ref({
mj: [],
sd: [],
@@ -298,7 +185,6 @@ const isOver = ref(false);
const imgType = ref("mj"); // 图片类别
const listBoxHeight = window.innerHeight - 124;
const colWidth = ref(220);
const fullImgHeight = ref(window.innerHeight - 60);
const showTaskDialog = ref(false);
const item = ref({});

View File

@@ -6,9 +6,7 @@
<el-menu mode="horizontal" :ellipsis="false">
<div class="menu-item">
<!-- <el-image :src="logo" class="logo" alt="Geek-AI" /> -->
<div class="logo-box">
<img src="@/assets/img/logo.png" alt="" />
</div>
<img :src="logo" class="logo" alt="" />
</div>
<div class="menu-item">
<span v-if="!license.de_copy">

View File

@@ -5,12 +5,7 @@
<AccountTop>
<template #default>
<div class="wechatLog flex-center" v-if="wechatLoginURL !== ''">
<a
:href="wechatLoginURL"
@click="setRoute(router.currentRoute.value.path)"
>
<i class="iconfont icon-wechat"></i>使用微信登录
</a>
<a :href="wechatLoginURL" @click="setRoute(router.currentRoute.value.path)"> <i class="iconfont icon-wechat"></i>使用微信登录 </a>
</div>
</template>
</AccountTop>
@@ -19,47 +14,26 @@
<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules">
<el-form-item label="" prop="username">
<div class="form-title">账号</div>
<el-input
v-model="ruleForm.username"
size="large"
placeholder="请输入账号"
@keyup="handleKeyup"
/>
<el-input v-model="ruleForm.username" size="large" placeholder="请输入账号" @keyup="handleKeyup" />
</el-form-item>
<el-form-item label="" prop="password">
<div class="flex-between w100">
<div class="form-title">密码</div>
<div
class="form-forget text-color-primary"
@click="router.push('/resetpassword')"
>
忘记密码
</div>
<div class="form-forget text-color-primary" @click="router.push('/resetpassword')">忘记密码</div>
</div>
<el-input
size="large"
v-model="ruleForm.password"
placeholder="请输入密码"
show-password
autocomplete="off"
@keyup="handleKeyup"
/>
<el-input size="large" v-model="ruleForm.password" placeholder="请输入密码" show-password autocomplete="off" @keyup="handleKeyup" />
</el-form-item>
<el-form-item>
<el-button
class="login-btn"
size="large"
type="primary"
@click="login"
>登录</el-button
>
<el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
<account-bg />
<captcha v-if="enableVerify" @success="doLogin" ref="captchaRef" />
</div>
</template>
@@ -76,6 +50,7 @@ import { setRoute } from "@/store/system";
import { useSharedStore } from "@/store/sharedata";
import AccountTop from "@/components/AccountTop.vue";
import Captcha from "@/components/Captcha.vue";
const router = useRouter();
const title = ref("Geek-AI");
@@ -87,16 +62,14 @@ const licenseConfig = ref({});
const wechatLoginURL = ref("");
const enableVerify = ref(false);
const captchaRef = ref(null);
// 是否需要验证码,输入一次密码错之后就要验证码
const needVerify = ref(false);
const ruleFormRef = ref(null);
const ruleForm = reactive({
username: process.env.VUE_APP_USER,
password: process.env.VUE_APP_PASS
password: process.env.VUE_APP_PASS,
});
const rules = {
username: [{ required: true, trigger: "blur", message: "请输入账号" }],
password: [{ required: true, trigger: "blur", message: "请输入密码" }]
password: [{ required: true, trigger: "blur", message: "请输入密码" }],
};
onMounted(() => {
// 获取系统配置
@@ -147,7 +120,7 @@ const handleKeyup = (e) => {
const login = async function () {
await ruleFormRef.value.validate(async (valid) => {
if (valid) {
if (enableVerify.value && needVerify.value) {
if (enableVerify.value) {
captchaRef.value.loadCaptcha();
} else {
doLogin({});
@@ -163,12 +136,11 @@ const doLogin = (verifyData) => {
password: password.value.trim(),
key: verifyData.key,
dots: verifyData.dots,
x: verifyData.x
x: verifyData.x,
})
.then((res) => {
setUserToken(res.data.token);
store.setIsLogin(true);
needVerify.value = false;
if (isMobile()) {
router.push("/mobile");
} else {
@@ -177,7 +149,6 @@ const doLogin = (verifyData) => {
})
.catch((e) => {
showMessageError("登录失败," + e.message);
needVerify.value = true;
});
};
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="index container">
<h2 class="title">{{title}}</h2>
<van-notice-bar left-icon="info-o" :scrollable="true">{{slogan}}}</van-notice-bar>
<h2 class="title">{{ title }}</h2>
<van-notice-bar left-icon="info-o" :scrollable="true">{{ slogan }}}</van-notice-bar>
<div class="content">
<van-grid :column-num="3" :gutter="10" border>
@@ -11,7 +11,6 @@
</template>
<template #text>
<div class="text">AI 对话</div>
</template>
</van-grid-item>
@@ -35,32 +34,25 @@
</van-grid>
<div class="app-list">
<van-list
v-model:loading="loading"
:finished="true"
finished-text=""
@load="fetchApps"
>
<van-list v-model:loading="loading" :finished="true" finished-text="" @load="fetchApps">
<van-cell v-for="item in apps" :key="item.id">
<div>
<div class="item">
<div class="image">
<div class="image flex justify-center items-center">
<van-image :src="item.icon" />
</div>
<div class="info">
<div class="info-title">{{item.name}}</div>
<div class="info-text">{{item.hello_msg}}</div>
<div class="info-title">{{ item.name }}</div>
<div class="info-text">{{ item.hello_msg }}</div>
</div>
</div>
<div class="btn">
<div v-if="hasRole(item.key)">
<van-button size="small" type="success" @click="useRole(item.id)">使用</van-button>
<van-button size="small" type="danger" @click="updateRole(item,'remove')">移除</van-button>
<van-button size="small" type="danger" @click="updateRole(item, 'remove')">移除</van-button>
</div>
<van-button v-else size="small"
style="--el-color-primary:#009999"
@click="updateRole(item, 'add')">
<van-button v-else size="small" style="--el-color-primary: #009999" @click="updateRole(item, 'add')">
<van-icon name="add-o" />
<span>添加应用</span>
</van-button>
@@ -74,91 +66,98 @@
</template>
<script setup>
import {onMounted, ref} from "vue";
import {useRouter} from "vue-router";
import {checkSession, getSystemInfo} from "@/store/cache";
import {httpGet, httpPost} from "@/utils/http";
import {arrayContains, removeArrayItem, showLoginDialog, substr} from "@/utils/libs";
import {showNotify} from "vant";
import {ElMessage} from "element-plus";
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { checkSession, getSystemInfo } from "@/store/cache";
import { httpGet, httpPost } from "@/utils/http";
import { arrayContains, removeArrayItem, showLoginDialog, substr } from "@/utils/libs";
import { showNotify } from "vant";
import { ElMessage } from "element-plus";
const title = ref(process.env.VUE_APP_TITLE)
const router = useRouter()
const isLogin = ref(false)
const apps = ref([])
const loading = ref(false)
const roles = ref([])
const slogan = ref('你有多大想象力AI就有多大创造力')
const title = ref(process.env.VUE_APP_TITLE);
const router = useRouter();
const isLogin = ref(false);
const apps = ref([]);
const loading = ref(false);
const roles = ref([]);
const slogan = ref("你有多大想象力AI就有多大创造力");
onMounted(() => {
getSystemInfo().then(res => {
title.value = res.data.title
if (res.data.slogan) {
slogan.value = res.data.slogan
}
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
getSystemInfo()
.then((res) => {
title.value = res.data.title;
if (res.data.slogan) {
slogan.value = res.data.slogan;
}
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
checkSession().then((user) => {
isLogin.value = true
roles.value = user.chat_roles
}).catch(() => {
})
fetchApps()
})
checkSession()
.then((user) => {
isLogin.value = true;
roles.value = user.chat_roles;
})
.catch(() => {});
fetchApps();
});
const fetchApps = () => {
httpGet("/api/app/list").then((res) => {
const items = res.data
// 处理 hello message
for (let i = 0; i < items.length; i++) {
items[i].intro = substr(items[i].hello_msg, 80)
}
apps.value = items
}).catch(e => {
showNotify({type:"danger", message:"获取应用失败:" + e.message})
})
}
httpGet("/api/app/list")
.then((res) => {
const items = res.data;
// 处理 hello message
for (let i = 0; i < items.length; i++) {
items[i].intro = substr(items[i].hello_msg, 80);
}
apps.value = items;
})
.catch((e) => {
showNotify({ type: "danger", message: "获取应用失败:" + e.message });
});
};
const updateRole = (row, opt) => {
if (!isLogin.value) {
return showLoginDialog(router)
return showLoginDialog(router);
}
const title = ref("")
const title = ref("");
if (opt === "add") {
title.value = "添加应用"
const exists = arrayContains(roles.value, row.key)
title.value = "添加应用";
const exists = arrayContains(roles.value, row.key);
if (exists) {
return
return;
}
roles.value.push(row.key)
roles.value.push(row.key);
} else {
title.value = "移除应用"
const exists = arrayContains(roles.value, row.key)
title.value = "移除应用";
const exists = arrayContains(roles.value, row.key);
if (!exists) {
return
return;
}
roles.value = removeArrayItem(roles.value, row.key)
roles.value = removeArrayItem(roles.value, row.key);
}
httpPost("/api/role/update", {keys: roles.value}).then(() => {
ElMessage.success({message: title.value + "成功!", duration: 1000})
}).catch(e => {
ElMessage.error(title.value + "失败:" + e.message)
})
}
httpPost("/api/role/update", { keys: roles.value })
.then(() => {
ElMessage.success({ message: title.value + "成功!", duration: 1000 });
})
.catch((e) => {
ElMessage.error(title.value + "失败:" + e.message);
});
};
const hasRole = (roleKey) => {
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
}
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2);
};
const useRole = (roleId) => {
if (!isLogin.value) {
return showLoginDialog(router)
return showLoginDialog(router);
}
router.push(`/mobile/chat/session?role_id=${roleId}`)
}
router.push(`/mobile/chat/session?role_id=${roleId}`);
};
</script>
<style scoped lang="stylus">

View File

@@ -0,0 +1,43 @@
<template>
<div class="login flex w-full flex-col place-content-center h-lvh">
<el-image src="/images/logo.png" class="w-1/2 mx-auto logo" />
<div class="title text-center text-3xl font-bold mt-8">{{ title }}</div>
<login-dialog @success="loginSuccess" />
</div>
</template>
<script setup>
import LoginDialog from "@/components/LoginDialog.vue";
import { getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { ref, onMounted } from "vue";
const router = useRouter();
const title = ref("登录");
const loginSuccess = () => {
router.back();
};
onMounted(() => {
getSystemInfo().then((res) => {
title.value = res.data.title;
});
});
</script>
<style scoped lang="stylus">
.login {
background: var(--theme-bg);
transition: all 0.3s ease;
.logo {
background: #ffffff;
border-radius: 50%;
}
.title {
color: var(--text-theme-color);
}
}
</style>