fix: 标准标签
This commit is contained in:
2
.env
2
.env
@@ -1,5 +1,5 @@
|
|||||||
# 标题
|
# 标题
|
||||||
VITE_APP_TITLE=衣朵云管理系统
|
VITE_APP_TITLE=Ai 决策通
|
||||||
|
|
||||||
# 项目本地运行端口号
|
# 项目本地运行端口号
|
||||||
VITE_PORT=80
|
VITE_PORT=80
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta
|
<meta
|
||||||
name="keywords"
|
name="keywords"
|
||||||
content="衣朵云管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
|
content="Ai 决策通 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="衣朵云管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
|
content="Ai 决策通 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
|
||||||
/>
|
/>
|
||||||
<title>%VITE_APP_TITLE%</title>
|
<title>%VITE_APP_TITLE%</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
92
src/api/ydoyun/tagconfig/index.ts
Normal file
92
src/api/ydoyun/tagconfig/index.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
/** 标签同步配置 */
|
||||||
|
export interface TagConfig {
|
||||||
|
id?: number
|
||||||
|
databaseId?: number
|
||||||
|
cronExpression?: string
|
||||||
|
/** 是否开启自动同步 */
|
||||||
|
autoSync?: boolean
|
||||||
|
lastSyncTime?: string
|
||||||
|
nextSyncTime?: string
|
||||||
|
/** 剔除大类(多选,逗号分隔) */
|
||||||
|
productCategoryIds?: string
|
||||||
|
/** 剔除仓库(多选,逗号分隔) */
|
||||||
|
syncWarehouseIds?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标准标签 */
|
||||||
|
export interface Tag {
|
||||||
|
id?: number
|
||||||
|
name?: string
|
||||||
|
type?: string
|
||||||
|
expression?: string
|
||||||
|
color?: string
|
||||||
|
sqlScript?: string
|
||||||
|
/** 已拼接参数的SQL(执行时直接使用,不展示) */
|
||||||
|
sqlScriptResolved?: string
|
||||||
|
useParams?: boolean
|
||||||
|
params?: string
|
||||||
|
/** 参数值 JSON,如 {"参数1":"28","参数2":"90"} */
|
||||||
|
paramValues?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标签同步历史 */
|
||||||
|
export interface TagSyncHistory {
|
||||||
|
id?: number
|
||||||
|
tagConfigId?: number
|
||||||
|
syncType?: string
|
||||||
|
syncTime?: string
|
||||||
|
syncStatus?: string
|
||||||
|
recordCount?: number
|
||||||
|
errorMessage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统一保存请求(配置+标签) */
|
||||||
|
export interface TagConfigSaveAllReq {
|
||||||
|
config?: TagConfig | null
|
||||||
|
tags?: Tag[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagConfigApi = {
|
||||||
|
getTagConfigPage: (params: any) =>
|
||||||
|
request.get({ url: '/ydoyun/tag-config/page', params }),
|
||||||
|
getTagConfig: (id: number) =>
|
||||||
|
request.get({ url: '/ydoyun/tag-config/get?id=' + id }),
|
||||||
|
getTagConfigByDatabaseId: (databaseId: number) =>
|
||||||
|
request.get({ url: '/ydoyun/tag-config/get-by-database?databaseId=' + databaseId }),
|
||||||
|
getTagConfigByTenant: () =>
|
||||||
|
request.get({ url: '/ydoyun/tag-config/get-by-tenant' }),
|
||||||
|
saveAll: (data: TagConfigSaveAllReq) =>
|
||||||
|
request.post({ url: '/ydoyun/tag-config/save', data }),
|
||||||
|
createTagConfig: (data: TagConfig) =>
|
||||||
|
request.post({ url: '/ydoyun/tag-config/create', data }),
|
||||||
|
updateTagConfig: (data: TagConfig) =>
|
||||||
|
request.put({ url: '/ydoyun/tag-config/update', data }),
|
||||||
|
deleteTagConfig: (id: number) =>
|
||||||
|
request.delete({ url: '/ydoyun/tag-config/delete?id=' + id }),
|
||||||
|
manualSync: (tagConfigId: number) =>
|
||||||
|
request.post({ url: '/ydoyun/tag-config/manual-sync?tagConfigId=' + tagConfigId }),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagApi = {
|
||||||
|
getTagPage: (params: any) =>
|
||||||
|
request.get({ url: '/ydoyun/tag/page', params }),
|
||||||
|
getTag: (id: number) =>
|
||||||
|
request.get({ url: '/ydoyun/tag/get?id=' + id }),
|
||||||
|
getTagListByType: (type?: string) =>
|
||||||
|
request.get({ url: '/ydoyun/tag/list-by-type', params: { type } }),
|
||||||
|
createTag: (data: Tag) =>
|
||||||
|
request.post({ url: '/ydoyun/tag/create', data }),
|
||||||
|
updateTag: (data: Tag) =>
|
||||||
|
request.put({ url: '/ydoyun/tag/update', data }),
|
||||||
|
deleteTag: (id: number) =>
|
||||||
|
request.delete({ url: '/ydoyun/tag/delete?id=' + id }),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagSyncHistoryApi = {
|
||||||
|
getTagSyncHistoryPage: (params: any) =>
|
||||||
|
request.get({ url: '/ydoyun/tag-sync-history/page', params }),
|
||||||
|
getTagSyncHistory: (id: number) =>
|
||||||
|
request.get({ url: '/ydoyun/tag-sync-history/get?id=' + id }),
|
||||||
|
}
|
||||||
@@ -66,10 +66,13 @@ watch(
|
|||||||
]"
|
]"
|
||||||
to="/"
|
to="/"
|
||||||
>
|
>
|
||||||
<img
|
<svg
|
||||||
class="h-[calc(var(--logo-height)-10px)] w-[calc(var(--logo-height)-10px)]"
|
class="logo-svg h-[calc(var(--logo-height)-10px)] w-[calc(var(--logo-height)-10px)]"
|
||||||
src="@/assets/imgs/logo.png"
|
viewBox="0 0 100 100"
|
||||||
/>
|
>
|
||||||
|
<path d="M50 2L2 25.5v49L50 98l48-23.5v-49L50 2zm35.2 65.7L50 88.3 14.8 67.7V32.3L50 11.7l35.2 20.6v35.4z" />
|
||||||
|
<path d="M38.1 48.9h23.8L50 28.3 38.1 48.9zM50 71.7l11.9-20.6H38.1L50 71.7z" />
|
||||||
|
</svg>
|
||||||
<div
|
<div
|
||||||
v-if="show"
|
v-if="show"
|
||||||
:class="[
|
:class="[
|
||||||
@@ -86,3 +89,10 @@ watch(
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logo-svg {
|
||||||
|
fill: var(--logo-title-text-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default {
|
|||||||
small: '小'
|
small: '小'
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
welcome: '衣朵云管理系统',
|
welcome: 'Ai 决策通',
|
||||||
message: '开箱即用的管理系统',
|
message: '开箱即用的管理系统',
|
||||||
tenantname: '租户名称',
|
tenantname: '租户名称',
|
||||||
username: '用户名',
|
username: '用户名',
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ const router = createRouter({
|
|||||||
history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#,createWebHistory URL不带#
|
history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#,createWebHistory URL不带#
|
||||||
strict: true,
|
strict: true,
|
||||||
routes: remainingRouter as RouteRecordRaw[],
|
routes: remainingRouter as RouteRecordRaw[],
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 })
|
scrollBehavior: () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve({ left: 0, top: 0 }), 0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export const resetRouter = (): void => {
|
export const resetRouter = (): void => {
|
||||||
|
|||||||
@@ -1,111 +1,393 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="login-page">
|
||||||
:class="prefixCls"
|
<!-- 动态背景 -->
|
||||||
class="relative h-[100%] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px lt-xl:px-10px"
|
<div class="bg-container">
|
||||||
>
|
<div class="blob blob-1"></div>
|
||||||
<div class="relative mx-auto h-full flex">
|
<div class="blob blob-2"></div>
|
||||||
<div
|
<div class="grid-base"></div>
|
||||||
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
|
<div class="flow-line h-line" style="top: 20%; left: -150px; animation-delay: 0s"></div>
|
||||||
>
|
<div class="flow-line h-line" style="top: 60%; left: -150px; animation-delay: 3s"></div>
|
||||||
<!-- 左上角的 logo + 系统标题 -->
|
<div class="flow-line v-line" style="left: 30%; top: -150px; animation-delay: 1.5s"></div>
|
||||||
<div class="relative flex items-center text-white">
|
|
||||||
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
|
|
||||||
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 左边的背景图 + 欢迎语 -->
|
|
||||||
<div class="h-[calc(100%-60px)] flex items-center justify-center">
|
<!-- 右上角主题、语言 -->
|
||||||
<TransitionGroup
|
<div class="login-header-actions">
|
||||||
appear
|
|
||||||
enter-active-class="animate__animated animate__bounceInLeft"
|
|
||||||
tag="div"
|
|
||||||
>
|
|
||||||
<img key="1" alt="" class="w-350px" src="@/assets/svgs/001.png" />
|
|
||||||
<div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div>
|
|
||||||
<div key="3" class="mt-5 text-14px font-normal text-white">
|
|
||||||
{{ t('login.message') }}
|
|
||||||
</div>
|
|
||||||
</TransitionGroup>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px overflow-x-hidden overflow-y-auto"
|
|
||||||
>
|
|
||||||
<!-- 右上角的主题、语言选择 -->
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between at-2xl:justify-end at-xl:justify-end"
|
|
||||||
style="color: var(--el-text-color-primary);"
|
|
||||||
>
|
|
||||||
<div class="flex items-center at-2xl:hidden at-xl:hidden">
|
|
||||||
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
|
|
||||||
<span class="text-20px font-bold" >{{ underlineToHump(appStore.getTitle) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-end space-x-10px h-48px">
|
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
<LocaleDropdown />
|
<LocaleDropdown />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<div class="container">
|
||||||
|
<!-- 左侧面板 -->
|
||||||
|
<div class="left-panel">
|
||||||
|
<div class="brand-box">
|
||||||
|
<div class="logo-main">
|
||||||
|
<svg viewBox="0 0 100 100" class="logo-svg">
|
||||||
|
<path d="M50 2L2 25.5v49L50 98l48-23.5v-49L50 2zm35.2 65.7L50 88.3 14.8 67.7V32.3L50 11.7l35.2 20.6v35.4z" />
|
||||||
|
<path d="M38.1 48.9h23.8L50 28.3 38.1 48.9zM50 71.7l11.9-20.6H38.1L50 71.7z" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<!-- 右边的登录界面 -->
|
<div class="brand-name">Ai 决策通</div>
|
||||||
<Transition appear enter-active-class="animate__animated animate__bounceInRight">
|
|
||||||
<div
|
|
||||||
class="m-auto h-[calc(100%-60px)] w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
|
|
||||||
>
|
|
||||||
<!-- 账号登录 -->
|
|
||||||
<LoginForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
<h2 class="sub-title">服鞋行业AI决策管理平台</h2>
|
||||||
|
<p class="desc">
|
||||||
|
专业的服鞋供应链 AI 决策中心。通过实时大数据分析与行业领先的智能算法,助力用户实现高效配补货决策及数字化经营管理。
|
||||||
|
</p>
|
||||||
|
<div class="features">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="icon-box">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14h-2v-4h2v4zm4 0h-2V7h2v10zm-8 0H8v-7h2v7z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
|
AI智能配补货
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="icon-box">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M3.5 18.49l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L2 17.08l1.5 1.41z"/></svg>
|
||||||
|
</div>
|
||||||
|
AI报表平台
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="icon-box">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M22 2H2v20h20V2zM10 7h4v2h-4V7zm8 10H6v-2h12v2zm0-4H6v-2h12v2zm0-4h-2V7h2v2z"/></svg>
|
||||||
|
</div>
|
||||||
|
AI管理工具包
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="icon-box">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/></svg>
|
||||||
|
</div>
|
||||||
|
AI智能标签
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧登录面板 -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="login-card">
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
长沙决策通科技有限公司 © 2026 版权所有
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
|
||||||
import { underlineToHump } from '@/utils'
|
|
||||||
|
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
<script lang="ts" setup>
|
||||||
import { useAppStore } from '@/store/modules/app'
|
|
||||||
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
|
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
|
||||||
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
|
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
|
||||||
|
import { LoginForm } from './components'
|
||||||
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue, ForgetPasswordForm } from './components'
|
|
||||||
|
|
||||||
defineOptions({ name: 'Login' })
|
defineOptions({ name: 'Login' })
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const appStore = useAppStore()
|
|
||||||
const { getPrefixCls } = useDesign()
|
|
||||||
const prefixCls = getPrefixCls('login')
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$prefix-cls: #{$namespace}-login;
|
.login-page {
|
||||||
|
--primary-blue: #004fb0;
|
||||||
|
--btn-grad: linear-gradient(180deg, #8cbaff 0%, #3e81e0 100%);
|
||||||
|
--glass: rgba(255, 255, 255, 0.35);
|
||||||
|
--grid-size: 70px;
|
||||||
|
|
||||||
.#{$prefix-cls} {
|
position: relative;
|
||||||
overflow: auto;
|
min-height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
&__left {
|
.bg-container {
|
||||||
&::before {
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-base {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
inset: 0;
|
||||||
left: 0;
|
background-image: linear-gradient(rgba(0, 79, 176, 0.08) 1.5px, transparent 1.5px),
|
||||||
z-index: -1;
|
linear-gradient(90deg, rgba(0, 79, 176, 0.08) 1.5px, transparent 1.5px);
|
||||||
|
background-size: var(--grid-size) var(--grid-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blob {
|
||||||
|
position: absolute;
|
||||||
|
filter: blur(80px);
|
||||||
|
opacity: 0.5;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.blob-1 {
|
||||||
|
width: 700px;
|
||||||
|
height: 700px;
|
||||||
|
background: #9ec9ff;
|
||||||
|
top: -15%;
|
||||||
|
left: -5%;
|
||||||
|
}
|
||||||
|
.blob-2 {
|
||||||
|
width: 600px;
|
||||||
|
height: 600px;
|
||||||
|
background: #7fb4ff;
|
||||||
|
bottom: -15%;
|
||||||
|
right: -5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-line {
|
||||||
|
position: absolute;
|
||||||
|
background: linear-gradient(90deg, transparent, #4388e4, transparent);
|
||||||
|
z-index: 2;
|
||||||
|
border-radius: 2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.h-line {
|
||||||
|
height: 1.5px;
|
||||||
|
width: 150px;
|
||||||
|
animation: h-move 7s linear infinite;
|
||||||
|
}
|
||||||
|
.v-line {
|
||||||
|
width: 1.5px;
|
||||||
|
height: 150px;
|
||||||
|
background: linear-gradient(180deg, transparent, #4388e4, transparent);
|
||||||
|
animation: v-move 9s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes h-move {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
10%,
|
||||||
|
90% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100vw);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes v-move {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
10%,
|
||||||
|
90% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(100vh);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header-actions {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 24px;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
max-width: 1300px;
|
||||||
background-image: url('@/assets/svgs/login-bg.svg');
|
margin: 0 auto;
|
||||||
background-position: center;
|
padding: 80px 40px 100px;
|
||||||
background-repeat: no-repeat;
|
display: flex;
|
||||||
content: '';
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
flex: 1.3;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-main {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background: linear-gradient(135deg, #0052d4, #4364f7);
|
||||||
|
border-radius: 14px;
|
||||||
|
margin-right: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 82, 212, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--primary-blue);
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
color: #5c6e82;
|
||||||
|
font-size: 15px;
|
||||||
|
max-width: 500px;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 260px);
|
||||||
|
gap: 20px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid white;
|
||||||
|
padding: 18px 22px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: 0.3s;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: #2a5fb2;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
flex: 0.7;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
width: 420px;
|
||||||
|
background: var(--glass);
|
||||||
|
backdrop-filter: blur(30px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 40px;
|
||||||
|
padding: 50px 45px;
|
||||||
|
box-shadow: 0 40px 80px rgba(0, 79, 176, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #94a3b8;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.left-panel {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
.right-panel {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.brand-name {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
.sub-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.features {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.login-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
padding: 40px 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- 暗色主题下字体颜色跟随反转 -->
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.dark .login-form {
|
.dark .login-page {
|
||||||
.el-divider__text {
|
.brand-name {
|
||||||
background-color: var(--login-bg-color);
|
color: var(--el-color-primary-light-3);
|
||||||
}
|
}
|
||||||
|
.sub-title {
|
||||||
.el-card {
|
color: var(--el-text-color-primary);
|
||||||
background-color: var(--login-bg-color);
|
}
|
||||||
|
.desc {
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
.feature-card {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.login-card {
|
||||||
|
background: var(--el-bg-color-overlay);
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,71 +4,68 @@
|
|||||||
ref="formLogin"
|
ref="formLogin"
|
||||||
:model="loginData.loginForm"
|
:model="loginData.loginForm"
|
||||||
:rules="LoginRules"
|
:rules="LoginRules"
|
||||||
class="login-form"
|
class="login-form-styled"
|
||||||
label-position="top"
|
label-position="top"
|
||||||
label-width="120px"
|
label-width="0"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<el-row class="mx-[-10px]">
|
<div class="login-header">
|
||||||
<el-col :span="24" class="px-10px">
|
<div class="welcome-text">欢迎登录</div>
|
||||||
<el-form-item>
|
</div>
|
||||||
<LoginFormTitle class="w-full" />
|
|
||||||
</el-form-item>
|
<!-- 租户名 -->
|
||||||
</el-col>
|
<div v-if="loginData.tenantEnable === 'true'" class="input-group">
|
||||||
<el-col :span="24" class="px-10px">
|
<el-form-item prop="tenantName">
|
||||||
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginData.loginForm.tenantName"
|
v-model="loginData.loginForm.tenantName"
|
||||||
:placeholder="t('login.tenantNamePlaceholder')"
|
:placeholder="t('login.tenantNamePlaceholder')"
|
||||||
:prefix-icon="iconHouse"
|
class="styled-input"
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</div>
|
||||||
<el-col :span="24" class="px-10px">
|
|
||||||
|
<!-- 用户名 -->
|
||||||
|
<div class="input-group">
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginData.loginForm.username"
|
v-model="loginData.loginForm.username"
|
||||||
:placeholder="t('login.usernamePlaceholder')"
|
:placeholder="t('login.usernamePlaceholder')"
|
||||||
:prefix-icon="iconAvatar"
|
class="styled-input"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</div>
|
||||||
<el-col :span="24" class="px-10px">
|
|
||||||
|
<!-- 密码 -->
|
||||||
|
<div class="input-group">
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginData.loginForm.password"
|
v-model="loginData.loginForm.password"
|
||||||
:placeholder="t('login.passwordPlaceholder')"
|
:placeholder="t('login.passwordPlaceholder')"
|
||||||
:prefix-icon="iconLock"
|
class="styled-input"
|
||||||
show-password
|
show-password
|
||||||
type="password"
|
type="password"
|
||||||
@keyup.enter="getCode()"
|
@keyup.enter="getCode()"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</div>
|
||||||
<el-col :span="24" class="px-10px mt-[-20px] mb-[-20px]">
|
|
||||||
<el-form-item>
|
<!-- 记住我 -->
|
||||||
<el-row justify="space-between" style="width: 100%">
|
<div class="links-row">
|
||||||
<el-col :span="6">
|
|
||||||
<el-checkbox v-model="loginData.loginForm.rememberMe">
|
<el-checkbox v-model="loginData.loginForm.rememberMe">
|
||||||
{{ t('login.remember') }}
|
{{ t('login.remember') }}
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</el-col>
|
</div>
|
||||||
</el-row>
|
|
||||||
</el-form-item>
|
<!-- 登录按钮 -->
|
||||||
</el-col>
|
<button
|
||||||
<el-col :span="24" class="px-10px">
|
type="button"
|
||||||
<el-form-item>
|
class="login-btn"
|
||||||
<XButton
|
:disabled="loginLoading"
|
||||||
:loading="loginLoading"
|
|
||||||
:title="t('login.login')"
|
|
||||||
class="w-full"
|
|
||||||
type="primary"
|
|
||||||
@click="getCode()"
|
@click="getCode()"
|
||||||
/>
|
>
|
||||||
</el-form-item>
|
{{ loginLoading ? '登录中...' : t('login.login') }}
|
||||||
</el-col>
|
</button>
|
||||||
|
|
||||||
<Verify
|
<Verify
|
||||||
v-if="loginData.captchaEnable === 'true'"
|
v-if="loginData.captchaEnable === 'true'"
|
||||||
ref="verify"
|
ref="verify"
|
||||||
@@ -77,16 +74,14 @@
|
|||||||
mode="pop"
|
mode="pop"
|
||||||
@success="handleLogin"
|
@success="handleLogin"
|
||||||
/>
|
/>
|
||||||
</el-row>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ElLoading } from 'element-plus'
|
import { ElLoading } from 'element-plus'
|
||||||
import LoginFormTitle from './LoginFormTitle.vue'
|
|
||||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||||
|
|
||||||
import { useIcon } from '@/hooks/web/useIcon'
|
import { required } from '@/utils/formRules'
|
||||||
|
|
||||||
import * as authUtil from '@/utils/auth'
|
import * as authUtil from '@/utils/auth'
|
||||||
import { usePermissionStore } from '@/store/modules/permission'
|
import { usePermissionStore } from '@/store/modules/permission'
|
||||||
import * as LoginApi from '@/api/login'
|
import * as LoginApi from '@/api/login'
|
||||||
@@ -96,28 +91,27 @@ defineOptions({ name: 'LoginForm' })
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const iconHouse = useIcon({ icon: 'ep:house' })
|
|
||||||
const iconAvatar = useIcon({ icon: 'ep:avatar' })
|
|
||||||
const iconLock = useIcon({ icon: 'ep:lock' })
|
|
||||||
const formLogin = ref()
|
const formLogin = ref()
|
||||||
const { validForm } = useFormValid(formLogin)
|
const { validForm } = useFormValid(formLogin)
|
||||||
const { setLoginState, getLoginState } = useLoginState()
|
const { getLoginState } = useLoginState()
|
||||||
const { currentRoute, push } = useRouter()
|
const { currentRoute, push } = useRouter()
|
||||||
const permissionStore = usePermissionStore()
|
const permissionStore = usePermissionStore()
|
||||||
const redirect = ref<string>('')
|
const redirect = ref<string>('')
|
||||||
const loginLoading = ref(false)
|
const loginLoading = ref(false)
|
||||||
const verify = ref()
|
const verify = ref()
|
||||||
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字 pictureWord 文字验证码
|
const captchaType = ref('blockPuzzle')
|
||||||
|
|
||||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
|
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
|
||||||
|
|
||||||
const LoginRules = {
|
const LoginRules: Record<string, any> = {
|
||||||
tenantName: [required],
|
|
||||||
username: [required],
|
username: [required],
|
||||||
password: [required]
|
password: [required]
|
||||||
}
|
}
|
||||||
|
if (import.meta.env.VITE_APP_TENANT_ENABLE === 'true') {
|
||||||
|
LoginRules.tenantName = [required]
|
||||||
|
}
|
||||||
|
|
||||||
const loginData = reactive({
|
const loginData = reactive({
|
||||||
isShowPassword: false,
|
|
||||||
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
|
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
|
||||||
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
|
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
|
||||||
loginForm: {
|
loginForm: {
|
||||||
@@ -125,49 +119,38 @@ const loginData = reactive({
|
|||||||
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
|
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
|
||||||
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
|
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
|
||||||
captchaVerification: '',
|
captchaVerification: '',
|
||||||
rememberMe: true // 默认记录我。如果不需要,可手动修改
|
rememberMe: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const socialList = [
|
|
||||||
{ icon: 'ant-design:wechat-filled', type: 30 },
|
|
||||||
{ icon: 'ant-design:dingtalk-circle-filled', type: 20 },
|
|
||||||
{ icon: 'ant-design:github-filled', type: 0 },
|
|
||||||
{ icon: 'ant-design:alipay-circle-filled', type: 0 }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 获取验证码
|
|
||||||
const getCode = async () => {
|
const getCode = async () => {
|
||||||
// 情况一,未开启:则直接登录
|
|
||||||
if (loginData.captchaEnable === 'false') {
|
if (loginData.captchaEnable === 'false') {
|
||||||
await handleLogin({})
|
await handleLogin({})
|
||||||
} else {
|
} else {
|
||||||
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
|
verify.value?.show()
|
||||||
// 弹出验证码
|
|
||||||
verify.value.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 获取租户 ID
|
|
||||||
const getTenantId = async () => {
|
const getTenantId = async () => {
|
||||||
if (loginData.tenantEnable === 'true') {
|
if (loginData.tenantEnable === 'true') {
|
||||||
const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
|
const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
|
||||||
authUtil.setTenantId(res)
|
authUtil.setTenantId(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 记住我
|
|
||||||
const getLoginFormCache = () => {
|
const getLoginFormCache = () => {
|
||||||
const loginForm = authUtil.getLoginForm()
|
const loginForm = authUtil.getLoginForm()
|
||||||
if (loginForm) {
|
if (loginForm) {
|
||||||
loginData.loginForm = {
|
loginData.loginForm = {
|
||||||
...loginData.loginForm,
|
...loginData.loginForm,
|
||||||
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
|
username: loginForm.username ?? loginData.loginForm.username,
|
||||||
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
|
password: loginForm.password ?? loginData.loginForm.password,
|
||||||
rememberMe: loginForm.rememberMe,
|
rememberMe: loginForm.rememberMe ?? true,
|
||||||
tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
|
tenantName: loginForm.tenantName ?? loginData.loginForm.tenantName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 根据域名,获得租户信息
|
|
||||||
const getTenantByWebsite = async () => {
|
const getTenantByWebsite = async () => {
|
||||||
if (loginData.tenantEnable === 'true') {
|
if (loginData.tenantEnable === 'true') {
|
||||||
const website = location.host
|
const website = location.host
|
||||||
@@ -178,8 +161,9 @@ const getTenantByWebsite = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const loading = ref() // ElLoading.service 返回的实例
|
|
||||||
// 登录
|
let loading: any = null
|
||||||
|
|
||||||
const handleLogin = async (params: any) => {
|
const handleLogin = async (params: any) => {
|
||||||
loginLoading.value = true
|
loginLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -188,19 +172,19 @@ const handleLogin = async (params: any) => {
|
|||||||
if (!data) {
|
if (!data) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const loginDataLoginForm = { ...loginData.loginForm }
|
const loginFormData = { ...loginData.loginForm }
|
||||||
loginDataLoginForm.captchaVerification = params.captchaVerification
|
loginFormData.captchaVerification = params.captchaVerification
|
||||||
const res = await LoginApi.login(loginDataLoginForm)
|
const res = await LoginApi.login(loginFormData)
|
||||||
if (!res) {
|
if (!res) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loading.value = ElLoading.service({
|
loading = ElLoading.service({
|
||||||
lock: true,
|
lock: true,
|
||||||
text: '正在加载系统中...',
|
text: '正在加载系统中...',
|
||||||
background: 'rgba(0, 0, 0, 0.7)'
|
background: 'rgba(0, 0, 0, 0.7)'
|
||||||
})
|
})
|
||||||
if (loginDataLoginForm.rememberMe) {
|
if (loginFormData.rememberMe) {
|
||||||
authUtil.setLoginForm(loginDataLoginForm)
|
authUtil.setLoginForm(loginFormData)
|
||||||
} else {
|
} else {
|
||||||
authUtil.removeLoginForm()
|
authUtil.removeLoginForm()
|
||||||
}
|
}
|
||||||
@@ -208,7 +192,6 @@ const handleLogin = async (params: any) => {
|
|||||||
if (!redirect.value) {
|
if (!redirect.value) {
|
||||||
redirect.value = '/'
|
redirect.value = '/'
|
||||||
}
|
}
|
||||||
// 判断是否为SSO登录
|
|
||||||
if (redirect.value.indexOf('sso') !== -1) {
|
if (redirect.value.indexOf('sso') !== -1) {
|
||||||
window.location.href = window.location.href.replace('/login?redirect=', '')
|
window.location.href = window.location.href.replace('/login?redirect=', '')
|
||||||
} else {
|
} else {
|
||||||
@@ -216,54 +199,18 @@ const handleLogin = async (params: any) => {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loginLoading.value = false
|
loginLoading.value = false
|
||||||
loading.value.close()
|
loading?.close?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 社交登录
|
|
||||||
const doSocialLogin = async (type: number) => {
|
|
||||||
if (type === 0) {
|
|
||||||
message.error('此方式未配置')
|
|
||||||
} else {
|
|
||||||
loginLoading.value = true
|
|
||||||
if (loginData.tenantEnable === 'true') {
|
|
||||||
// 尝试先通过 tenantName 获取租户
|
|
||||||
await getTenantId()
|
|
||||||
// 如果获取不到,则需要弹出提示,进行处理
|
|
||||||
if (!authUtil.getTenantId()) {
|
|
||||||
try {
|
|
||||||
const data = await message.prompt('请输入租户名称', t('common.reminder'))
|
|
||||||
if (data?.action !== 'confirm') throw 'cancel'
|
|
||||||
const res = await LoginApi.getTenantIdByName(data.value)
|
|
||||||
authUtil.setTenantId(res)
|
|
||||||
} catch (error) {
|
|
||||||
if (error === 'cancel') return
|
|
||||||
} finally {
|
|
||||||
loginLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 计算 redirectUri
|
|
||||||
// 注意: type、redirect 需要先 encode 一次,否则钉钉回调会丢失。
|
|
||||||
// 配合 social-login.vue#getUrlValue() 使用
|
|
||||||
const redirectUri =
|
|
||||||
location.origin +
|
|
||||||
'/social-login?' +
|
|
||||||
encodeURIComponent(`type=${type}&redirect=${redirect.value || '/'}`)
|
|
||||||
|
|
||||||
// 进行跳转
|
|
||||||
window.location.href = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
watch(
|
watch(
|
||||||
() => currentRoute.value,
|
() => currentRoute.value,
|
||||||
(route: RouteLocationNormalizedLoaded) => {
|
(route: RouteLocationNormalizedLoaded) => {
|
||||||
redirect.value = route?.query?.redirect as string
|
redirect.value = route?.query?.redirect as string
|
||||||
},
|
},
|
||||||
{
|
{ immediate: true }
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getLoginFormCache()
|
getLoginFormCache()
|
||||||
getTenantByWebsite()
|
getTenantByWebsite()
|
||||||
@@ -271,23 +218,119 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
:deep(.anticon) {
|
.login-form-styled {
|
||||||
&:hover {
|
:deep(.el-form-item) {
|
||||||
color: var(--el-color-primary) !important;
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
:deep(.el-form-item__error) {
|
||||||
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-code {
|
.login-header {
|
||||||
float: right;
|
margin-bottom: 40px;
|
||||||
width: 100%;
|
}
|
||||||
height: 38px;
|
|
||||||
|
|
||||||
img {
|
.welcome-text {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
:deep(.styled-input) {
|
||||||
|
.el-input__wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
padding: 18px 22px;
|
||||||
max-width: 100px;
|
border-radius: 15px;
|
||||||
vertical-align: middle;
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
font-size: 16px;
|
||||||
|
transition: 0.3s;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.el-input__wrapper:hover {
|
||||||
|
border-color: rgba(0, 79, 176, 0.3);
|
||||||
|
}
|
||||||
|
.el-input__wrapper.is-focus {
|
||||||
|
border-color: #004fb0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.el-input__inner,
|
||||||
|
input {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333 !important;
|
||||||
|
-webkit-text-fill-color: #333 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.links-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 15px 0 40px;
|
||||||
|
|
||||||
|
:deep(.el-checkbox__label) {
|
||||||
|
color: #6e7c8d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 18px;
|
||||||
|
background: linear-gradient(180deg, #8cbaff 0%, #3e81e0 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 35px;
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-shadow: 0 12px 25px rgba(62, 129, 224, 0.35);
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
.login-btn:hover:not(:disabled) {
|
||||||
|
transform: scale(1.02);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
.login-btn:disabled {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- 暗色主题下登录表单字体颜色跟随反转 -->
|
||||||
|
<style lang="scss">
|
||||||
|
.dark .login-form-styled {
|
||||||
|
.welcome-text {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
.links-row :deep(.el-checkbox__label) {
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
.input-group :deep(.el-input__wrapper) {
|
||||||
|
background: var(--el-fill-color);
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
}
|
||||||
|
/* 已输入文字颜色 - 确保暗色下清晰可见 */
|
||||||
|
.input-group :deep(.el-input__inner),
|
||||||
|
.input-group :deep(input) {
|
||||||
|
color: var(--el-text-color-primary) !important;
|
||||||
|
-webkit-text-fill-color: var(--el-text-color-primary) !important;
|
||||||
|
}
|
||||||
|
.input-group :deep(input::placeholder) {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
}
|
||||||
|
.input-group :deep(.el-input__wrapper.is-focus) {
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.input-group :deep(.el-input__wrapper.is-focus .el-input__inner),
|
||||||
|
.input-group :deep(.el-input__wrapper.is-focus input) {
|
||||||
|
color: var(--el-text-color-primary) !important;
|
||||||
|
-webkit-text-fill-color: var(--el-text-color-primary) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
192
src/views/ydoyun/tagconfig/TagForm.vue
Normal file
192
src/views/ydoyun/tagconfig/TagForm.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="120px"
|
||||||
|
v-loading="formLoading"
|
||||||
|
>
|
||||||
|
<el-form-item label="标签名称" prop="name">
|
||||||
|
<el-input v-model="formData.name" placeholder="请输入标签名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签类型" prop="type">
|
||||||
|
<el-select v-model="formData.type" placeholder="请选择标签类型" class="!w-full">
|
||||||
|
<el-option label="产品基础标签 (product)" value="product" />
|
||||||
|
<el-option label="门店基础标签 (store)" value="store" />
|
||||||
|
<el-option label="供应商基础标签 (supplier)" value="supplier" />
|
||||||
|
<el-option label="会员基础标签 (member)" value="member" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签表达式" prop="expression">
|
||||||
|
<el-input v-model="formData.expression" type="textarea" placeholder="请输入标签表达式" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签颜色" prop="color">
|
||||||
|
<div class="color-selector-row">
|
||||||
|
<div class="dot-group">
|
||||||
|
<span
|
||||||
|
v-for="c in presetColors"
|
||||||
|
:key="c"
|
||||||
|
class="color-dot color-dot-clickable"
|
||||||
|
:class="{ 'color-dot-active': formData.color === c }"
|
||||||
|
:style="{ backgroundColor: c }"
|
||||||
|
:title="'预设色 ' + c"
|
||||||
|
@click="formData.color = c"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<el-color-picker v-model="formData.color" :predefine="presetColors" size="default" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="执行SQL脚本" prop="sqlScript">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.sqlScript"
|
||||||
|
type="textarea"
|
||||||
|
:rows="10"
|
||||||
|
placeholder="请输入标签同步时执行的SQL脚本,支持多行"
|
||||||
|
class="sql-script-area"
|
||||||
|
/>
|
||||||
|
<div class="form-item-hint">该SQL将用于标签计算与同步,请确保语法正确</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否代入参数" prop="useParams">
|
||||||
|
<el-radio-group v-model="formData.useParams">
|
||||||
|
<el-radio :value="true">是</el-radio>
|
||||||
|
<el-radio :value="false">否</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="参数列表" prop="params">
|
||||||
|
<el-input v-model="formData.params" placeholder="逗号分隔,如:param1,param2" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { TagApi } from '@/api/ydoyun/tagconfig'
|
||||||
|
import type { Tag } from '@/api/ydoyun/tagconfig'
|
||||||
|
|
||||||
|
defineOptions({ name: 'TagForm' })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('')
|
||||||
|
const formLoading = ref(false)
|
||||||
|
const formType = ref('')
|
||||||
|
const formData = ref<Tag>({
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
type: undefined,
|
||||||
|
expression: undefined,
|
||||||
|
color: undefined,
|
||||||
|
sqlScript: undefined,
|
||||||
|
useParams: false,
|
||||||
|
params: undefined,
|
||||||
|
})
|
||||||
|
const formRules = reactive({
|
||||||
|
name: [{ required: true, message: '标签名称不能为空', trigger: 'blur' }],
|
||||||
|
type: [{ required: true, message: '请选择标签类型', trigger: 'change' }],
|
||||||
|
sqlScript: [{ required: true, message: '执行SQL脚本不能为空', trigger: 'blur' }],
|
||||||
|
useParams: [{ required: true, message: '请选择是否代入参数', trigger: 'change' }],
|
||||||
|
})
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
/** 4 个预设颜色(蓝、红、黄、绿) */
|
||||||
|
const presetColors = ['#2f54eb', '#f5222d', '#fa8c16', '#52c41a']
|
||||||
|
|
||||||
|
const open = async (type: string, id?: number, initialType?: string) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogTitle.value = type === 'create' ? '新增标签' : '编辑标签'
|
||||||
|
formType.value = type
|
||||||
|
resetForm()
|
||||||
|
if (id) {
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await TagApi.getTag(id)
|
||||||
|
formData.value = (res as any)?.data || {}
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
} else if (type === 'create' && initialType) {
|
||||||
|
formData.value.type = initialType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({ open })
|
||||||
|
|
||||||
|
const emit = defineEmits(['success'])
|
||||||
|
const submitForm = async () => {
|
||||||
|
await formRef.value.validate()
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = formData.value
|
||||||
|
if (formType.value === 'create') {
|
||||||
|
await TagApi.createTag(data)
|
||||||
|
message.success(t('common.createSuccess'))
|
||||||
|
} else {
|
||||||
|
await TagApi.updateTag(data)
|
||||||
|
message.success(t('common.updateSuccess'))
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
emit('success')
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
type: undefined,
|
||||||
|
expression: undefined,
|
||||||
|
color: undefined,
|
||||||
|
sqlScript: undefined,
|
||||||
|
useParams: false,
|
||||||
|
params: undefined,
|
||||||
|
}
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.color-selector-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.dot-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.color-dot {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 0 0 0 1px #d9d9d9;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.color-dot-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
.color-dot-clickable:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
.color-dot-active {
|
||||||
|
box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.sql-script-area :deep(textarea) {
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.form-item-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
2229
src/views/ydoyun/tagconfig/index.vue
Normal file
2229
src/views/ydoyun/tagconfig/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user