Compare commits
3 Commits
1cc427cbb0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d8144bf87 | |||
| 2f5ee49594 | |||
| a0afedf5f3 |
@@ -1,3 +1,2 @@
|
||||
/node_modules/*
|
||||
/dist/*
|
||||
/src/main.ts
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"tabWidth": 4,
|
||||
"useTabs": true,
|
||||
"semi": false,
|
||||
"vueIndentScriptAndStyle": true,
|
||||
"singleQuote": true,
|
||||
|
||||
+1
-1
@@ -42,6 +42,6 @@
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+3
-13
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"name": "art-design-pro",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.19.0",
|
||||
"pnpm": ">=8.8.0"
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --open",
|
||||
@@ -13,16 +12,7 @@
|
||||
"lint": "eslint",
|
||||
"fix": "eslint --fix",
|
||||
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
|
||||
"lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix",
|
||||
"lint:lint-staged": "lint-staged",
|
||||
"prepare": "husky",
|
||||
"commit": "git-cz",
|
||||
"clean:dev": "tsx scripts/clean-dev.ts"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-git"
|
||||
}
|
||||
"lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,mjs,mts,tsx}": [
|
||||
|
||||
+16
-16
@@ -4,31 +4,31 @@
|
||||
</ElConfigProvider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from './store/modules/user'
|
||||
import zh from 'element-plus/es/locale/lang/zh-cn'
|
||||
import en from 'element-plus/es/locale/lang/en'
|
||||
import { systemUpgrade } from './utils/sys'
|
||||
import { toggleTransition } from './utils/ui/animation'
|
||||
import { checkStorageCompatibility } from './utils/storage'
|
||||
import { initializeTheme } from './hooks/core/useTheme'
|
||||
<script setup>
|
||||
import { useUserStore } from './store/modules/user'
|
||||
import zh from 'element-plus/es/locale/lang/zh-cn'
|
||||
import en from 'element-plus/es/locale/lang/en'
|
||||
import { systemUpgrade } from './utils/sys'
|
||||
import { toggleTransition } from './utils/ui/animation'
|
||||
import { checkStorageCompatibility } from './utils/storage'
|
||||
import { initializeTheme } from './hooks/core/useTheme'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { language } = storeToRefs(userStore)
|
||||
const userStore = useUserStore()
|
||||
const { language } = storeToRefs(userStore)
|
||||
|
||||
const locales = {
|
||||
const locales = {
|
||||
zh: zh,
|
||||
en: en
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
onBeforeMount(() => {
|
||||
toggleTransition(true)
|
||||
initializeTheme()
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(() => {
|
||||
checkStorageCompatibility()
|
||||
toggleTransition(false)
|
||||
systemUpgrade()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* @param params 登录参数
|
||||
* @returns 登录响应
|
||||
*/
|
||||
export function fetchLogin(params) {
|
||||
return request.post({
|
||||
url: '/api/auth/login',
|
||||
params
|
||||
// showSuccessMessage: true // 显示成功消息
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @returns 用户信息
|
||||
*/
|
||||
export function fetchGetUserInfo() {
|
||||
return request.get({
|
||||
url: '/api/user/info'
|
||||
// 自定义请求头
|
||||
// headers: {
|
||||
// 'X-Custom-Header': 'your-custom-value'
|
||||
// }
|
||||
})
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* @param params 登录参数
|
||||
* @returns 登录响应
|
||||
*/
|
||||
export function fetchLogin(params: Api.Auth.LoginParams) {
|
||||
return request.post<Api.Auth.LoginResponse>({
|
||||
url: '/api/auth/login',
|
||||
params
|
||||
// showSuccessMessage: true // 显示成功消息
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @returns 用户信息
|
||||
*/
|
||||
export function fetchGetUserInfo() {
|
||||
return request.get<Api.Auth.UserInfo>({
|
||||
url: '/api/user/info'
|
||||
// 自定义请求头
|
||||
// headers: {
|
||||
// 'X-Custom-Header': 'your-custom-value'
|
||||
// }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
// 获取用户列表
|
||||
export function fetchGetUserList(params) {
|
||||
return request.get({
|
||||
url: '/api/user/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取角色列表
|
||||
export function fetchGetRoleList(params) {
|
||||
return request.get({
|
||||
url: '/api/role/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取菜单列表
|
||||
export function fetchGetMenuList() {
|
||||
return request.get({
|
||||
url: '/api/v3/system/menus/simple'
|
||||
})
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import request from '@/utils/http'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
|
||||
// 获取用户列表
|
||||
export function fetchGetUserList(params: Api.SystemManage.UserSearchParams) {
|
||||
return request.get<Api.SystemManage.UserList>({
|
||||
url: '/api/user/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取角色列表
|
||||
export function fetchGetRoleList(params: Api.SystemManage.RoleSearchParams) {
|
||||
return request.get<Api.SystemManage.RoleList>({
|
||||
url: '/api/role/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取菜单列表
|
||||
export function fetchGetMenuList() {
|
||||
return request.get<AppRouteRecord[]>({
|
||||
url: '/api/v3/system/menus/simple'
|
||||
})
|
||||
}
|
||||
@@ -151,7 +151,8 @@
|
||||
var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate)
|
||||
var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate)
|
||||
var(--tw-backdrop-sepia);
|
||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast)
|
||||
var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert)
|
||||
var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness)
|
||||
var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate)
|
||||
var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate)
|
||||
var(--tw-backdrop-sepia);
|
||||
}
|
||||
|
||||
@@ -24,14 +24,19 @@
|
||||
<div class="basic-banner__content">
|
||||
<!-- title slot -->
|
||||
<slot name="title">
|
||||
<p v-if="title" class="basic-banner__title" :style="{ color: titleColor }">{{ title }}</p>
|
||||
<p v-if="title" class="basic-banner__title" :style="{ color: titleColor }">{{
|
||||
title
|
||||
}}</p>
|
||||
</slot>
|
||||
|
||||
<!-- subtitle slot -->
|
||||
<slot name="subtitle">
|
||||
<p v-if="subtitle" class="basic-banner__subtitle" :style="{ color: subtitleColor }">{{
|
||||
subtitle
|
||||
}}</p>
|
||||
<p
|
||||
v-if="subtitle"
|
||||
class="basic-banner__subtitle"
|
||||
:style="{ color: subtitleColor }"
|
||||
>{{ subtitle }}</p
|
||||
>
|
||||
</slot>
|
||||
|
||||
<!-- button slot -->
|
||||
@@ -58,7 +63,11 @@
|
||||
v-if="imageConfig.src"
|
||||
class="basic-banner__background-image"
|
||||
:src="imageConfig.src"
|
||||
:style="{ width: imageConfig.width, bottom: imageConfig.bottom, right: imageConfig.right }"
|
||||
:style="{
|
||||
width: imageConfig.width,
|
||||
bottom: imageConfig.bottom,
|
||||
right: imageConfig.right
|
||||
}"
|
||||
loading="lazy"
|
||||
alt="背景图片"
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-medium text-danger"
|
||||
:class="[percentage > 0 ? 'text-success' : '', isMiniChart ? 'absolute bottom-5' : '']"
|
||||
:class="[
|
||||
percentage > 0 ? 'text-success' : '',
|
||||
isMiniChart ? 'absolute bottom-5' : ''
|
||||
]"
|
||||
>
|
||||
{{ percentage > 0 ? '+' : '' }}{{ percentage }}%
|
||||
</div>
|
||||
@@ -21,7 +24,9 @@
|
||||
<div
|
||||
ref="chartRef"
|
||||
class="absolute bottom-0 left-0 right-0 mx-auto"
|
||||
:class="isMiniChart ? '!absolute !top-5 !right-5 !bottom-auto !left-auto !h-15 !w-4/10' : ''"
|
||||
:class="
|
||||
isMiniChart ? '!absolute !top-5 !right-5 !bottom-auto !left-auto !h-15 !w-4/10' : ''
|
||||
"
|
||||
:style="{ height: isMiniChart ? '60px' : `calc(${height}rem - 5rem)` }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
<div
|
||||
ref="chartRef"
|
||||
class="absolute bottom-0 left-0 right-0 box-border w-full"
|
||||
:class="isMiniChart ? '!absolute !top-5 !right-5 !bottom-auto !left-auto !h-15 !w-4/10' : ''"
|
||||
:class="
|
||||
isMiniChart ? '!absolute !top-5 !right-5 !bottom-auto !left-auto !h-15 !w-4/10' : ''
|
||||
"
|
||||
:style="{ height: isMiniChart ? '60px' : `calc(${height}rem - 5rem)` }"
|
||||
></div>
|
||||
</div>
|
||||
@@ -107,13 +109,15 @@
|
||||
offset: 0,
|
||||
color: props.color
|
||||
? hexToRgba(props.color, 0.2).rgba
|
||||
: hexToRgba(getCssVar('--el-color-primary'), 0.2).rgba
|
||||
: hexToRgba(getCssVar('--el-color-primary'), 0.2)
|
||||
.rgba
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: props.color
|
||||
? hexToRgba(props.color, 0.01).rgba
|
||||
: hexToRgba(getCssVar('--el-color-primary'), 0.01).rgba
|
||||
: hexToRgba(getCssVar('--el-color-primary'), 0.01)
|
||||
.rgba
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<!-- 进度条卡片 -->
|
||||
<template>
|
||||
<div class="art-card h-32 flex flex-col justify-center px-5">
|
||||
<div class="mb-3.5 flex-c" :style="{ justifyContent: icon ? 'space-between' : 'flex-start' }">
|
||||
<div
|
||||
class="mb-3.5 flex-c"
|
||||
:style="{ justifyContent: icon ? 'space-between' : 'flex-start' }"
|
||||
>
|
||||
<div v-if="icon" class="size-11 flex-cc bg-g-300 text-xl rounded-lg" :class="iconStyle">
|
||||
<ArtSvgIcon :icon="icon" class="text-2xl"></ArtSvgIcon>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
class="art-card h-32 flex-c px-5 transition-transform duration-200 hover:-translate-y-0.5"
|
||||
:class="boxStyle"
|
||||
>
|
||||
<div v-if="icon" class="mr-4 size-11 flex-cc rounded-lg text-xl text-white" :class="iconStyle">
|
||||
<div
|
||||
v-if="icon"
|
||||
class="mr-4 size-11 flex-cc rounded-lg text-xl text-white"
|
||||
:class="iconStyle"
|
||||
>
|
||||
<ArtSvgIcon :icon="icon"></ArtSvgIcon>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
<div class="flex-c gap-3">
|
||||
<div class="flex-c gap-2">
|
||||
<span class="text-sm">{{ item.content }}</span>
|
||||
<span v-if="item.code" class="text-sm text-theme"> #{{ item.code }} </span>
|
||||
<span v-if="item.code" class="text-sm text-theme">
|
||||
#{{ item.code }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ElTimelineItem>
|
||||
|
||||
@@ -135,7 +135,9 @@
|
||||
const multiData = props.data as BarDataItem[]
|
||||
return (
|
||||
!multiData.length ||
|
||||
multiData.every((item) => !item.data?.length || item.data.every((val) => val === 0))
|
||||
multiData.every(
|
||||
(item) => !item.data?.length || item.data.every((val) => val === 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -144,11 +146,15 @@
|
||||
watchSources: [() => props.data, () => props.xAxisData, () => props.colors],
|
||||
generateOptions: (): EChartsOption => {
|
||||
const options: EChartsOption = {
|
||||
grid: getGridWithLegend(props.showLegend && isMultipleData.value, props.legendPosition, {
|
||||
grid: getGridWithLegend(
|
||||
props.showLegend && isMultipleData.value,
|
||||
props.legendPosition,
|
||||
{
|
||||
top: 15,
|
||||
right: 0,
|
||||
left: 0
|
||||
}),
|
||||
}
|
||||
),
|
||||
tooltip: props.showTooltip ? getTooltipStyle() : undefined,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
|
||||
@@ -139,7 +139,9 @@
|
||||
const multiData = props.data as BarDataItem[]
|
||||
return (
|
||||
!multiData.length ||
|
||||
multiData.every((item) => !item.data?.length || item.data.every((val) => val === 0))
|
||||
multiData.every(
|
||||
(item) => !item.data?.length || item.data.every((val) => val === 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -148,11 +150,15 @@
|
||||
watchSources: [() => props.data, () => props.xAxisData, () => props.colors],
|
||||
generateOptions: (): EChartsOption => {
|
||||
const options: EChartsOption = {
|
||||
grid: getGridWithLegend(props.showLegend && isMultipleData.value, props.legendPosition, {
|
||||
grid: getGridWithLegend(
|
||||
props.showLegend && isMultipleData.value,
|
||||
props.legendPosition,
|
||||
{
|
||||
top: 15,
|
||||
right: 0,
|
||||
left: 0
|
||||
}),
|
||||
}
|
||||
),
|
||||
tooltip: props.showTooltip ? getTooltipStyle() : undefined,
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
return (
|
||||
!props.data?.length ||
|
||||
props.data.every(
|
||||
(item) => item.open === 0 && item.close === 0 && item.high === 0 && item.low === 0
|
||||
(item) =>
|
||||
item.open === 0 && item.close === 0 && item.high === 0 && item.low === 0
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -112,7 +113,12 @@
|
||||
series: [
|
||||
{
|
||||
type: 'candlestick',
|
||||
data: props.data.map((item) => [item.open, item.close, item.low, item.high]),
|
||||
data: props.data.map((item) => [
|
||||
item.open,
|
||||
item.close,
|
||||
item.low,
|
||||
item.high
|
||||
]),
|
||||
itemStyle: {
|
||||
color: upColor,
|
||||
color0: downColor,
|
||||
|
||||
@@ -193,11 +193,15 @@
|
||||
animation: true,
|
||||
animationDuration: isInitial ? 0 : 1300,
|
||||
animationDurationUpdate: isInitial ? 0 : 1300,
|
||||
grid: getGridWithLegend(props.showLegend && isMultipleData.value, props.legendPosition, {
|
||||
grid: getGridWithLegend(
|
||||
props.showLegend && isMultipleData.value,
|
||||
props.legendPosition,
|
||||
{
|
||||
top: 15,
|
||||
right: 15,
|
||||
left: 0
|
||||
}),
|
||||
}
|
||||
),
|
||||
tooltip: props.showTooltip ? getTooltipStyle() : undefined,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
@@ -358,7 +362,9 @@
|
||||
}
|
||||
|
||||
// 使用 VueUse 的 watchDebounced 优化数据监听(避免频繁更新)
|
||||
watch([() => props.data, () => props.xAxisData, () => props.colors], renderChart, { deep: true })
|
||||
watch([() => props.data, () => props.xAxisData, () => props.colors], renderChart, {
|
||||
deep: true
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
|
||||
@@ -36,7 +36,10 @@
|
||||
const { chartRef, isDark, getAnimationConfig, getTooltipStyle } = useChartComponent({
|
||||
props,
|
||||
checkEmpty: () => {
|
||||
return !props.data?.length || props.data.every((item) => item.value.every((val) => val === 0))
|
||||
return (
|
||||
!props.data?.length ||
|
||||
props.data.every((item) => item.value.every((val) => val === 0))
|
||||
)
|
||||
},
|
||||
watchSources: [() => props.data, () => props.indicator, () => props.colors],
|
||||
generateOptions: (): EChartsOption => {
|
||||
|
||||
@@ -52,7 +52,10 @@
|
||||
} = useChartComponent({
|
||||
props,
|
||||
checkEmpty: () => {
|
||||
return !props.data?.length || props.data.every((item) => item.value.every((val) => val === 0))
|
||||
return (
|
||||
!props.data?.length ||
|
||||
props.data.every((item) => item.value.every((val) => val === 0))
|
||||
)
|
||||
},
|
||||
watchSources: [() => props.data, () => props.colors, () => props.symbolSize],
|
||||
generateOptions: (): EChartsOption => {
|
||||
@@ -96,13 +99,17 @@
|
||||
itemStyle: {
|
||||
color: computedColor,
|
||||
shadowBlur: 6,
|
||||
shadowColor: isDark.value ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
|
||||
shadowColor: isDark.value
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowOffsetY: 2
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 12,
|
||||
shadowColor: isDark.value ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'
|
||||
shadowColor: isDark.value
|
||||
? 'rgba(255, 255, 255, 0.2)'
|
||||
: 'rgba(0, 0, 0, 0.2)'
|
||||
},
|
||||
scale: true
|
||||
},
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<ElDropdown v-if="hasAnyAuthItem">
|
||||
<ArtIconButton icon="ri:more-2-fill" class="!size-8 bg-g-200 dark:bg-g-300/45 text-sm" />
|
||||
<ArtIconButton
|
||||
icon="ri:more-2-fill"
|
||||
class="!size-8 bg-g-200 dark:bg-g-300/45 text-sm"
|
||||
/>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<template v-for="item in list" :key="item.key">
|
||||
|
||||
@@ -238,7 +238,8 @@
|
||||
handler.value.style.transition = 'none'
|
||||
// 计算拖拽起始位置
|
||||
state.x =
|
||||
(e.pageX || e.touches[0].pageX) - parseInt(handler.value.style.left.replace('px', ''), 10)
|
||||
(e.pageX || e.touches[0].pageX) -
|
||||
parseInt(handler.value.style.left.replace('px', ''), 10)
|
||||
}
|
||||
emit('handlerMove')
|
||||
}
|
||||
|
||||
@@ -218,7 +218,9 @@
|
||||
|
||||
return columns.map((column) => {
|
||||
// 使用配置的列宽度
|
||||
const configWidth = Object.values(props.columns).find((col) => col.title === column)?.width
|
||||
const configWidth = Object.values(props.columns).find(
|
||||
(col) => col.title === column
|
||||
)?.width
|
||||
|
||||
if (configWidth) {
|
||||
return { wch: configWidth }
|
||||
@@ -310,7 +312,11 @@
|
||||
|
||||
return Promise.resolve()
|
||||
} catch (error) {
|
||||
throw new ExportError(`Excel 导出失败: ${(error as Error).message}`, 'EXPORT_FAILED', error)
|
||||
throw new ExportError(
|
||||
`Excel 导出失败: ${(error as Error).message}`,
|
||||
'EXPORT_FAILED',
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +350,11 @@
|
||||
const exportError =
|
||||
error instanceof ExportError
|
||||
? error
|
||||
: new ExportError(`导出失败: ${(error as Error).message}`, 'UNKNOWN_ERROR', error)
|
||||
: new ExportError(
|
||||
`导出失败: ${(error as Error).message}`,
|
||||
'UNKNOWN_ERROR',
|
||||
error
|
||||
)
|
||||
|
||||
// 触发错误事件
|
||||
emit('export-error', exportError)
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
</template>
|
||||
|
||||
<!-- 复选框组 -->
|
||||
<template v-if="item.type === 'checkboxgroup' && getProps(item)?.options">
|
||||
<template
|
||||
v-if="item.type === 'checkboxgroup' && getProps(item)?.options"
|
||||
>
|
||||
<ElCheckbox
|
||||
v-for="option in getProps(item).options"
|
||||
v-bind="option"
|
||||
@@ -52,7 +54,9 @@
|
||||
</template>
|
||||
|
||||
<!-- 单选框组 -->
|
||||
<template v-if="item.type === 'radiogroup' && getProps(item)?.options">
|
||||
<template
|
||||
v-if="item.type === 'radiogroup' && getProps(item)?.options"
|
||||
>
|
||||
<ElRadio
|
||||
v-for="option in getProps(item).options"
|
||||
v-bind="option"
|
||||
@@ -61,7 +65,11 @@
|
||||
</template>
|
||||
|
||||
<!-- 动态插槽支持 -->
|
||||
<template v-for="(slotFn, slotName) in getSlots(item)" :key="slotName" #[slotName]>
|
||||
<template
|
||||
v-for="(slotFn, slotName) in getSlots(item)"
|
||||
:key="slotName"
|
||||
#[slotName]
|
||||
>
|
||||
<component :is="slotFn" />
|
||||
</template>
|
||||
</component>
|
||||
@@ -74,7 +82,12 @@
|
||||
:style="actionButtonsStyle"
|
||||
>
|
||||
<div class="flex gap-2 md:justify-center">
|
||||
<ElButton v-if="showReset" class="reset-button" @click="handleReset" v-ripple>
|
||||
<ElButton
|
||||
v-if="showReset"
|
||||
class="reset-button"
|
||||
@click="handleReset"
|
||||
v-ripple
|
||||
>
|
||||
{{ t('table.form.reset') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
</template>
|
||||
|
||||
<!-- 复选框组 -->
|
||||
<template v-if="item.type === 'checkboxgroup' && getProps(item)?.options">
|
||||
<template
|
||||
v-if="item.type === 'checkboxgroup' && getProps(item)?.options"
|
||||
>
|
||||
<ElCheckbox
|
||||
v-for="option in getProps(item).options"
|
||||
v-bind="option"
|
||||
@@ -52,7 +54,9 @@
|
||||
</template>
|
||||
|
||||
<!-- 单选框组 -->
|
||||
<template v-if="item.type === 'radiogroup' && getProps(item)?.options">
|
||||
<template
|
||||
v-if="item.type === 'radiogroup' && getProps(item)?.options"
|
||||
>
|
||||
<ElRadio
|
||||
v-for="option in getProps(item).options"
|
||||
v-bind="option"
|
||||
@@ -61,7 +65,11 @@
|
||||
</template>
|
||||
|
||||
<!-- 动态插槽支持 -->
|
||||
<template v-for="(slotFn, slotName) in getSlots(item)" :key="slotName" #[slotName]>
|
||||
<template
|
||||
v-for="(slotFn, slotName) in getSlots(item)"
|
||||
:key="slotName"
|
||||
#[slotName]
|
||||
>
|
||||
<component :is="slotFn" />
|
||||
</template>
|
||||
</component>
|
||||
@@ -71,7 +79,12 @@
|
||||
<ElCol :xs="24" :sm="24" :md="span" :lg="span" :xl="span" class="action-column">
|
||||
<div class="action-buttons-wrapper" :style="actionButtonsStyle">
|
||||
<div class="form-buttons">
|
||||
<ElButton v-if="showReset" class="reset-button" @click="handleReset" v-ripple>
|
||||
<ElButton
|
||||
v-if="showReset"
|
||||
class="reset-button"
|
||||
@click="handleReset"
|
||||
v-ripple
|
||||
>
|
||||
{{ t('table.searchBar.reset') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@@ -85,7 +98,11 @@
|
||||
{{ t('table.searchBar.search') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div v-if="shouldShowExpandToggle" class="filter-toggle" @click="toggleExpand">
|
||||
<div
|
||||
v-if="shouldShowExpandToggle"
|
||||
class="filter-toggle"
|
||||
@click="toggleExpand"
|
||||
>
|
||||
<span>{{ expandToggleText }}</span>
|
||||
<div class="icon-wrapper">
|
||||
<ElIcon>
|
||||
@@ -298,7 +315,9 @@
|
||||
const shouldShowExpandToggle = computed(() => {
|
||||
const filteredItems = props.items.filter((item) => !item.hidden)
|
||||
return (
|
||||
!props.isExpand && props.showExpand && filteredItems.length > Math.floor(24 / props.span) - 1
|
||||
!props.isExpand &&
|
||||
props.showExpand &&
|
||||
filteredItems.length > Math.floor(24 / props.span) - 1
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
// 计算属性:上传服务器地址
|
||||
const uploadServer = computed(
|
||||
() =>
|
||||
props.uploadConfig?.server || `${import.meta.env.VITE_API_URL}/api/common/upload/wangeditor`
|
||||
props.uploadConfig?.server ||
|
||||
`${import.meta.env.VITE_API_URL}/api/common/upload/wangeditor`
|
||||
)
|
||||
|
||||
// 合并上传配置
|
||||
@@ -168,7 +169,9 @@
|
||||
}
|
||||
|
||||
const toolbar = editorContainer.querySelector('.w-e-toolbar')
|
||||
const toolbarButtons = editorContainer.querySelectorAll('.w-e-bar-item button[data-menu-key]')
|
||||
const toolbarButtons = editorContainer.querySelectorAll(
|
||||
'.w-e-bar-item button[data-menu-key]'
|
||||
)
|
||||
|
||||
if (toolbar && toolbarButtons.length > 0) {
|
||||
return
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<!-- 系统聊天窗口 -->
|
||||
<template>
|
||||
<div>
|
||||
<ElDrawer v-model="isDrawerVisible" :size="isMobile ? '100%' : '480px'" :with-header="false">
|
||||
<ElDrawer
|
||||
v-model="isDrawerVisible"
|
||||
:size="isMobile ? '100%' : '480px'"
|
||||
:with-header="false"
|
||||
>
|
||||
<div class="mb-5 flex-cb">
|
||||
<div>
|
||||
<span class="text-base font-medium">Art Bot</span>
|
||||
@@ -34,7 +38,10 @@
|
||||
>
|
||||
<ElAvatar :size="32" :src="message.avatar" class="shrink-0" />
|
||||
<div
|
||||
:class="['flex max-w-[70%] flex-col', message.isMe ? 'items-end' : 'items-start']"
|
||||
:class="[
|
||||
'flex max-w-[70%] flex-col',
|
||||
message.isMe ? 'items-end' : 'items-start'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
@@ -48,7 +55,9 @@
|
||||
<div
|
||||
:class="[
|
||||
'rounded-md px-3.5 py-2.5 text-sm leading-[1.4] text-g-900',
|
||||
message.isMe ? 'message-right bg-theme/15' : 'message-left bg-g-300/50'
|
||||
message.isMe
|
||||
? 'message-right bg-theme/15'
|
||||
: 'message-left bg-g-300/50'
|
||||
]"
|
||||
>{{ message.content }}</div
|
||||
>
|
||||
@@ -71,16 +80,23 @@
|
||||
<div class="flex gap-2 py-2">
|
||||
<ElButton :icon="Paperclip" circle plain />
|
||||
<ElButton :icon="Picture" circle plain />
|
||||
<ElButton type="primary" @click="sendMessage" v-ripple>发送</ElButton>
|
||||
<ElButton type="primary" @click="sendMessage" v-ripple
|
||||
>发送</ElButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElInput>
|
||||
<div class="mt-3 flex-cb">
|
||||
<div class="flex-c">
|
||||
<ArtSvgIcon icon="ri:image-line" class="mr-5 c-p text-g-600 text-lg" />
|
||||
<ArtSvgIcon icon="ri:emotion-happy-line" class="mr-5 c-p text-g-600 text-lg" />
|
||||
<ArtSvgIcon
|
||||
icon="ri:emotion-happy-line"
|
||||
class="mr-5 c-p text-g-600 text-lg"
|
||||
/>
|
||||
</div>
|
||||
<ElButton type="primary" @click="sendMessage" v-ripple class="min-w-20">发送</ElButton>
|
||||
<ElButton type="primary" @click="sendMessage" v-ripple class="min-w-20"
|
||||
>发送</ElButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,7 +162,8 @@
|
||||
{
|
||||
id: 3,
|
||||
sender: BOT_NAME,
|
||||
content: '好的,我来为您介绍系统的主要功能。首先,您可以通过左侧菜单访问不同的功能模块...',
|
||||
content:
|
||||
'好的,我来为您介绍系统的主要功能。首先,您可以通过左侧菜单访问不同的功能模块...',
|
||||
time: '10:02',
|
||||
isMe: false,
|
||||
avatar: aiAvatar
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
class="mr-3 c-p flex-c gap-3 rounded-lg p-2 hover:bg-g-200/70 dark:hover:bg-g-200/90 hover:[&_.app-icon]:!bg-transparent"
|
||||
@click="handleApplicationClick(application)"
|
||||
>
|
||||
<div class="app-icon size-12 flex-cc rounded-lg bg-g-200/80 dark:bg-g-300/30">
|
||||
<div
|
||||
class="app-icon size-12 flex-cc rounded-lg bg-g-200/80 dark:bg-g-300/30"
|
||||
>
|
||||
<ArtSvgIcon
|
||||
class="text-xl"
|
||||
:icon="application.icon"
|
||||
@@ -37,7 +39,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="m-0 text-sm font-medium text-g-800">{{ application.name }}</h3>
|
||||
<h3 class="m-0 text-sm font-medium text-g-800">{{
|
||||
application.name
|
||||
}}</h3>
|
||||
<p class="mt-1 text-xs text-g-600">{{ application.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -292,7 +292,8 @@
|
||||
const startY = this.canvasHeight
|
||||
|
||||
// 根据是否有图片确定可用形状
|
||||
const availableShapes = imageUrl && this.imageCache[imageUrl] ? ['image'] : CONFIG.SHAPES
|
||||
const availableShapes =
|
||||
imageUrl && this.imageCache[imageUrl] ? ['image'] : CONFIG.SHAPES
|
||||
|
||||
// 批量创建粒子数组,减少频繁的数组操作
|
||||
const particles: Firework[] = []
|
||||
@@ -310,7 +311,8 @@
|
||||
particle.x = startX
|
||||
particle.y = startY
|
||||
// 复杂的速度计算,模拟真实烟花爆炸轨迹
|
||||
particle.vx = Math.cos(angle) * Math.cos(spread) * speed * (Math.random() * 0.5 + 0.5)
|
||||
particle.vx =
|
||||
Math.cos(angle) * Math.cos(spread) * speed * (Math.random() * 0.5 + 0.5)
|
||||
particle.vy = Math.sin(angle) * speed - 15 // 向上初始速度
|
||||
particle.color = CONFIG.COLORS[Math.floor(Math.random() * CONFIG.COLORS.length)]
|
||||
particle.rotation = Math.random() * 360
|
||||
@@ -463,7 +465,15 @@
|
||||
case 'oval':
|
||||
// 绘制椭圆
|
||||
ctx.value.beginPath()
|
||||
ctx.value.ellipse(0, 0, SIZES.OVAL.WIDTH / 2, SIZES.OVAL.HEIGHT / 2, 0, 0, Math.PI * 2)
|
||||
ctx.value.ellipse(
|
||||
0,
|
||||
0,
|
||||
SIZES.OVAL.WIDTH / 2,
|
||||
SIZES.OVAL.HEIGHT / 2,
|
||||
0,
|
||||
0,
|
||||
Math.PI * 2
|
||||
)
|
||||
ctx.value.fill()
|
||||
break
|
||||
|
||||
|
||||
@@ -35,12 +35,17 @@
|
||||
>
|
||||
<div
|
||||
class="mt-2 h-12 flex-cb rounded-custom-sm bg-g-200/80 px-4 text-sm text-g-700"
|
||||
:class="isHighlighted(index) ? 'highlighted !bg-theme/70 !text-white' : ''"
|
||||
:class="
|
||||
isHighlighted(index) ? 'highlighted !bg-theme/70 !text-white' : ''
|
||||
"
|
||||
@click="searchGoPage(item)"
|
||||
@mouseenter="highlightOnHover(index)"
|
||||
>
|
||||
{{ formatMenuTitle(item.meta.title) }}
|
||||
<ArtSvgIcon v-show="isHighlighted(index)" icon="fluent:arrow-enter-left-20-filled" />
|
||||
<ArtSvgIcon
|
||||
v-show="isHighlighted(index)"
|
||||
icon="fluent:arrow-enter-left-20-filled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,16 +81,24 @@
|
||||
<div class="dialog-footer box-border flex-c border-t-d pt-4.5 pb-1">
|
||||
<div class="flex-cc">
|
||||
<ArtSvgIcon icon="fluent:arrow-enter-left-20-filled" class="keyboard" />
|
||||
<span class="mr-3.5 text-xs text-g-700">{{ $t('search.selectKeydown') }}</span>
|
||||
<span class="mr-3.5 text-xs text-g-700">{{
|
||||
$t('search.selectKeydown')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex-c">
|
||||
<ArtSvgIcon icon="ri:arrow-up-wide-fill" class="keyboard" />
|
||||
<ArtSvgIcon icon="ri:arrow-down-wide-fill" class="keyboard" />
|
||||
<span class="mr-3.5 text-xs text-g-700">{{ $t('search.switchKeydown') }}</span>
|
||||
<span class="mr-3.5 text-xs text-g-700">{{
|
||||
$t('search.switchKeydown')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex-c">
|
||||
<i class="keyboard !w-8 flex-cc"><p class="text-[10px] font-medium">ESC</p></i>
|
||||
<span class="mr-3.5 text-xs text-g-700">{{ $t('search.exitKeydown') }}</span>
|
||||
<i class="keyboard !w-8 flex-cc"
|
||||
><p class="text-[10px] font-medium">ESC</p></i
|
||||
>
|
||||
<span class="mr-3.5 text-xs text-g-700">{{
|
||||
$t('search.exitKeydown')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
<!-- 系统信息 -->
|
||||
<div class="flex-c c-p" @click="toHome" v-if="isTopMenu">
|
||||
<ArtLogo class="pl-4.5" />
|
||||
<p v-if="width >= 1400" class="my-0 mx-2 ml-2 text-lg">{{ AppConfig.systemInfo.name }}</p>
|
||||
<p v-if="width >= 1400" class="my-0 mx-2 ml-2 text-lg">{{
|
||||
AppConfig.systemInfo.name
|
||||
}}</p>
|
||||
</div>
|
||||
|
||||
<ArtLogo
|
||||
@@ -50,7 +52,9 @@
|
||||
|
||||
<!-- 面包屑 -->
|
||||
<ArtBreadcrumb
|
||||
v-if="(shouldShowBreadcrumb && isLeftMenu) || (shouldShowBreadcrumb && isDualMenu)"
|
||||
v-if="
|
||||
(shouldShowBreadcrumb && isLeftMenu) || (shouldShowBreadcrumb && isDualMenu)
|
||||
"
|
||||
/>
|
||||
|
||||
<!-- 顶部菜单 -->
|
||||
@@ -69,7 +73,9 @@
|
||||
>
|
||||
<div class="flex-c">
|
||||
<ArtSvgIcon icon="ri:search-line" class="text-sm text-g-500" />
|
||||
<span class="ml-1 text-xs font-normal text-g-500">{{ $t('topBar.search.title') }}</span>
|
||||
<span class="ml-1 text-xs font-normal text-g-500">{{
|
||||
$t('topBar.search.title')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex-c h-5 px-1.5 text-g-500/80 border border-g-400 rounded">
|
||||
<ArtSvgIcon v-if="isWindows" icon="vaadin:ctrl-a" class="text-sm" />
|
||||
@@ -96,7 +102,11 @@
|
||||
<ArtIconButton icon="ri:translate-2" class="language-btn text-[19px]" />
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<div v-for="item in languageOptions" :key="item.value" class="lang-btn-item">
|
||||
<div
|
||||
v-for="item in languageOptions"
|
||||
:key="item.value"
|
||||
class="lang-btn-item"
|
||||
>
|
||||
<ElDropdownItem
|
||||
:command="item.value"
|
||||
:class="{ 'is-selected': locale === item.value }"
|
||||
@@ -126,22 +136,36 @@
|
||||
class="chat-button relative"
|
||||
@click="openChat"
|
||||
>
|
||||
<div class="breathing-dot absolute top-2 right-2 size-1.5 !bg-success rounded-full"></div>
|
||||
<div
|
||||
class="breathing-dot absolute top-2 right-2 size-1.5 !bg-success rounded-full"
|
||||
></div>
|
||||
</ArtIconButton>
|
||||
|
||||
<!-- 设置按钮 -->
|
||||
<div v-if="shouldShowSettings">
|
||||
<ElPopover :visible="showSettingGuide" placement="bottom-start" :width="190" :offset="0">
|
||||
<ElPopover
|
||||
:visible="showSettingGuide"
|
||||
placement="bottom-start"
|
||||
:width="190"
|
||||
:offset="0"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="flex-cc">
|
||||
<ArtIconButton icon="ri:settings-line" class="setting-btn" @click="openSetting" />
|
||||
<ArtIconButton
|
||||
icon="ri:settings-line"
|
||||
class="setting-btn"
|
||||
@click="openSetting"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<p
|
||||
>{{ $t('topBar.guide.title')
|
||||
}}<span :style="{ color: systemThemeColor }"> {{ $t('topBar.guide.theme') }} </span
|
||||
>、 <span :style="{ color: systemThemeColor }"> {{ $t('topBar.guide.menu') }} </span
|
||||
}}<span :style="{ color: systemThemeColor }">
|
||||
{{ $t('topBar.guide.theme') }} </span
|
||||
>、
|
||||
<span :style="{ color: systemThemeColor }">
|
||||
{{ $t('topBar.guide.menu') }} </span
|
||||
>{{ $t('topBar.guide.description') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
<span class="block text-sm font-medium text-g-800 truncate">{{
|
||||
userInfo.userName
|
||||
}}</span>
|
||||
<span class="block mt-0.5 text-xs text-g-500 truncate">{{ userInfo.email }}</span>
|
||||
<span class="block mt-0.5 text-xs text-g-500 truncate">{{
|
||||
userInfo.email
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="py-4 mt-3 border-t border-g-300/80">
|
||||
|
||||
@@ -203,7 +203,10 @@
|
||||
? SCROLL_CONFIG.WHEEL_FAST_STEP
|
||||
: SCROLL_CONFIG.WHEEL_SLOW_STEP
|
||||
const scrollDelta = event.deltaY > 0 ? scrollStep : -scrollStep
|
||||
const targetScroll = Math.max(0, Math.min(scrollLeft + scrollDelta, scrollWidth - clientWidth))
|
||||
const targetScroll = Math.max(
|
||||
0,
|
||||
Math.min(scrollLeft + scrollDelta, scrollWidth - clientWidth)
|
||||
)
|
||||
|
||||
// 立即滚动,无动画
|
||||
wrapRef.scrollLeft = targetScroll
|
||||
|
||||
@@ -9,13 +9,20 @@
|
||||
<div
|
||||
v-if="isDualMenu"
|
||||
class="dual-menu-left"
|
||||
:style="{ width: dualMenuShowText ? '80px' : '64px', background: getMenuTheme.background }"
|
||||
:style="{
|
||||
width: dualMenuShowText ? '80px' : '64px',
|
||||
background: getMenuTheme.background
|
||||
}"
|
||||
>
|
||||
<ArtLogo class="logo" @click="navigateToHome" />
|
||||
|
||||
<ElScrollbar style="height: calc(100% - 135px)">
|
||||
<ul>
|
||||
<li v-for="menu in firstLevelMenus" :key="menu.path" @click="handleMenuJump(menu, true)">
|
||||
<li
|
||||
v-for="menu in firstLevelMenus"
|
||||
:key="menu.path"
|
||||
@click="handleMenuJump(menu, true)"
|
||||
>
|
||||
<ElTooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
|
||||
@@ -48,7 +48,10 @@
|
||||
{{ formatMenuTitle(item.meta.title) }}
|
||||
</span>
|
||||
<div v-if="item.meta.showBadge" class="art-badge" />
|
||||
<div v-if="item.meta.showTextBadge && (level > 0 || menuOpen)" class="art-text-badge">
|
||||
<div
|
||||
v-if="item.meta.showTextBadge && (level > 0 || menuOpen)"
|
||||
class="art-text-badge"
|
||||
>
|
||||
{{ item.meta.showTextBadge }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -41,10 +41,15 @@
|
||||
class="size-9 leading-9 text-center rounded-lg flex-cc"
|
||||
:class="[getNoticeStyle(item.type).iconClass]"
|
||||
>
|
||||
<ArtSvgIcon class="text-lg !bg-transparent" :icon="getNoticeStyle(item.type).icon" />
|
||||
<ArtSvgIcon
|
||||
class="text-lg !bg-transparent"
|
||||
:icon="getNoticeStyle(item.type).icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-[calc(100%-45px)] ml-3.5">
|
||||
<h4 class="text-sm font-normal leading-5.5 text-g-900">{{ item.title }}</h4>
|
||||
<h4 class="text-sm font-normal leading-5.5 text-g-900">{{
|
||||
item.title
|
||||
}}</h4>
|
||||
<p class="mt-1.5 text-xs text-g-500">{{ item.time }}</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
<div v-if="!isLock">
|
||||
<ElDialog v-model="visible" :width="370" :show-close="false" @open="handleDialogOpen">
|
||||
<div class="flex-c flex-col">
|
||||
<img class="w-16 h-16 rounded-full" src="@imgs/user/avatar.webp" alt="用户头像" />
|
||||
<img
|
||||
class="w-16 h-16 rounded-full"
|
||||
src="@imgs/user/avatar.webp"
|
||||
alt="用户头像"
|
||||
/>
|
||||
<div class="mt-7.5 mb-3.5 text-base font-medium">{{ userInfo.userName }}</div>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
@@ -59,7 +63,11 @@
|
||||
<!-- 解锁界面 -->
|
||||
<div v-else class="unlock-content">
|
||||
<div class="flex-c flex-col w-80">
|
||||
<img class="w-16 h-16 mt-5 rounded-full" src="@imgs/user/avatar.webp" alt="用户头像" />
|
||||
<img
|
||||
class="w-16 h-16 mt-5 rounded-full"
|
||||
src="@imgs/user/avatar.webp"
|
||||
alt="用户头像"
|
||||
/>
|
||||
<div class="mt-3 mb-3.5 text-base font-medium">
|
||||
{{ userInfo.userName }}
|
||||
</div>
|
||||
@@ -353,7 +361,10 @@
|
||||
|
||||
await formRef.value.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
const encryptedPassword = CryptoJS.AES.encrypt(formData.password, ENCRYPT_KEY).toString()
|
||||
const encryptedPassword = CryptoJS.AES.encrypt(
|
||||
formData.password,
|
||||
ENCRYPT_KEY
|
||||
).toString()
|
||||
userStore.setLockStatus(true)
|
||||
userStore.setLockPassword(encryptedPassword)
|
||||
visible.value = false
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
:key="item.value"
|
||||
@click="switchMenuLayouts(item.value)"
|
||||
>
|
||||
<div class="box" :class="{ 'is-active': item.value === menuType, 'mt-16': index > 2 }">
|
||||
<div
|
||||
class="box"
|
||||
:class="{ 'is-active': item.value === menuType, 'mt-16': index > 2 }"
|
||||
>
|
||||
<img :src="item.img" />
|
||||
</div>
|
||||
<p class="name">{{ $t(`setting.menuType.list[${index}]`) }}</p>
|
||||
|
||||
@@ -193,7 +193,9 @@
|
||||
toggleIfDifferent(settingStore.showRefreshButton, config.showRefreshButton, () =>
|
||||
settingStore.setShowRefreshButton()
|
||||
)
|
||||
toggleIfDifferent(settingStore.showCrumbs, config.showCrumbs, () => settingStore.setCrumbs())
|
||||
toggleIfDifferent(settingStore.showCrumbs, config.showCrumbs, () =>
|
||||
settingStore.setCrumbs()
|
||||
)
|
||||
toggleIfDifferent(settingStore.showLanguage, config.showLanguage, () =>
|
||||
settingStore.setLanguage()
|
||||
)
|
||||
@@ -207,11 +209,15 @@
|
||||
settingStore.setWatermarkVisible(config.watermarkVisible)
|
||||
|
||||
// 功能设置
|
||||
toggleIfDifferent(settingStore.autoClose, config.autoClose, () => settingStore.setAutoClose())
|
||||
toggleIfDifferent(settingStore.autoClose, config.autoClose, () =>
|
||||
settingStore.setAutoClose()
|
||||
)
|
||||
toggleIfDifferent(settingStore.uniqueOpened, config.uniqueOpened, () =>
|
||||
settingStore.setUniqueOpened()
|
||||
)
|
||||
toggleIfDifferent(settingStore.colorWeak, config.colorWeak, () => settingStore.setColorWeak())
|
||||
toggleIfDifferent(settingStore.colorWeak, config.colorWeak, () =>
|
||||
settingStore.setColorWeak()
|
||||
)
|
||||
|
||||
// 样式设置
|
||||
toggleIfDifferent(settingStore.boxBorderMode, config.boxBorderMode, () =>
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
<span class="text-sm">{{ config.label }}</span>
|
||||
|
||||
<!-- 开关类型 -->
|
||||
<ElSwitch v-if="config.type === 'switch'" :model-value="modelValue" @change="handleChange" />
|
||||
<ElSwitch
|
||||
v-if="config.type === 'switch'"
|
||||
:model-value="modelValue"
|
||||
@change="handleChange"
|
||||
/>
|
||||
|
||||
<!-- 数字输入类型 -->
|
||||
<ElInputNumber
|
||||
|
||||
@@ -21,8 +21,12 @@
|
||||
<li
|
||||
class="art-card-xs inline-flex flex-cc h-8 mr-1.5 text-xs c-p hover:text-theme group"
|
||||
:class="[
|
||||
item.path === activeTab ? 'activ-tab !text-theme' : 'text-g-600 dark:text-g-800',
|
||||
tabStyle === 'tab-google' ? 'google-tab relative !h-8 !leading-8 !border-none' : ''
|
||||
item.path === activeTab
|
||||
? 'activ-tab !text-theme'
|
||||
: 'text-g-600 dark:text-g-800',
|
||||
tabStyle === 'tab-google'
|
||||
? 'google-tab relative !h-8 !leading-8 !border-none'
|
||||
: ''
|
||||
]"
|
||||
:style="{
|
||||
padding: item.fixedTab ? '0 10px' : '0 8px 0 12px',
|
||||
@@ -143,7 +147,9 @@
|
||||
// 计算属性
|
||||
const list = computed(() => store.opened)
|
||||
const activeTab = computed(() => currentRoute.value.path)
|
||||
const activeTabIndex = computed(() => list.value.findIndex((tab) => tab.path === activeTab.value))
|
||||
const activeTabIndex = computed(() =>
|
||||
list.value.findIndex((tab) => tab.path === activeTab.value)
|
||||
)
|
||||
|
||||
// 右键菜单逻辑
|
||||
const useContextMenu = () => {
|
||||
@@ -168,15 +174,18 @@
|
||||
|
||||
return {
|
||||
areAllLeftTabsFixed: leftTabs.length > 0 && leftTabs.every((tab) => tab.fixedTab),
|
||||
areAllRightTabsFixed: rightTabs.length > 0 && rightTabs.every((tab) => tab.fixedTab),
|
||||
areAllOtherTabsFixed: otherTabs.length > 0 && otherTabs.every((tab) => tab.fixedTab),
|
||||
areAllRightTabsFixed:
|
||||
rightTabs.length > 0 && rightTabs.every((tab) => tab.fixedTab),
|
||||
areAllOtherTabsFixed:
|
||||
otherTabs.length > 0 && otherTabs.every((tab) => tab.fixedTab),
|
||||
areAllTabsFixed: list.value.every((tab) => tab.fixedTab)
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单选项
|
||||
const menuItems = computed(() => {
|
||||
const { clickedIndex, currentTab, isLastTab, isOneTab, isCurrentTab } = getClickedTabInfo()
|
||||
const { clickedIndex, currentTab, isLastTab, isOneTab, isCurrentTab } =
|
||||
getClickedTabInfo()
|
||||
const fixedStatus = checkTabsFixedStatus(clickedIndex)
|
||||
|
||||
return [
|
||||
@@ -266,7 +275,8 @@
|
||||
const { scrollWidth, ulWidth, offsetLeft, curTabRight, targetLeft } = positions
|
||||
|
||||
if (
|
||||
(offsetLeft > Math.abs(scrollState.value.translateX) && curTabRight <= scrollWidth) ||
|
||||
(offsetLeft > Math.abs(scrollState.value.translateX) &&
|
||||
curTabRight <= scrollWidth) ||
|
||||
(scrollState.value.translateX < targetLeft && targetLeft < 0)
|
||||
) {
|
||||
return
|
||||
@@ -313,7 +323,8 @@
|
||||
|
||||
const xMax = 0
|
||||
const xMin = scrollRef.value.offsetWidth - tabsRef.value.offsetWidth
|
||||
const delta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY
|
||||
const delta =
|
||||
Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY
|
||||
|
||||
scrollState.value.translateX = Math.min(
|
||||
Math.max(scrollState.value.translateX - delta, xMin),
|
||||
|
||||
@@ -57,7 +57,10 @@
|
||||
v-for="child in item.children"
|
||||
:key="child.key"
|
||||
class="menu-item relative mx-1.5 flex-c c-p select-none rounded text-xs transition-colors duration-150 hover:bg-g-200"
|
||||
:class="{ 'is-disabled': child.disabled, 'has-line': child.showLine }"
|
||||
:class="{
|
||||
'is-disabled': child.disabled,
|
||||
'has-line': child.showLine
|
||||
}"
|
||||
:style="menuItemStyle"
|
||||
@click="handleMenuClick(child)"
|
||||
>
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
@click="search"
|
||||
:class="showSearchBar ? 'active !bg-theme hover:!bg-theme/80' : ''"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:search-line" :class="showSearchBar ? 'text-white' : 'text-g-700'" />
|
||||
<ArtSvgIcon
|
||||
icon="ri:search-line"
|
||||
:class="showSearchBar ? 'text-white' : 'text-g-700'"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldShow('refresh')"
|
||||
@@ -50,7 +53,9 @@
|
||||
</ElDropdown>
|
||||
|
||||
<div v-if="shouldShow('fullscreen')" class="button" @click="toggleFullScreen">
|
||||
<ArtSvgIcon :icon="isFullScreen ? 'ri:fullscreen-exit-line' : 'ri:fullscreen-line'" />
|
||||
<ArtSvgIcon
|
||||
:icon="isFullScreen ? 'ri:fullscreen-exit-line' : 'ri:fullscreen-line'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 列设置 -->
|
||||
@@ -77,7 +82,9 @@
|
||||
>
|
||||
<div
|
||||
class="drag-icon mr-2 h-4.5 flex-cc text-g-500"
|
||||
:class="item.fixed ? 'cursor-default text-g-300' : 'cursor-move'"
|
||||
:class="
|
||||
item.fixed ? 'cursor-default text-g-300' : 'cursor-move'
|
||||
"
|
||||
>
|
||||
<ArtSvgIcon
|
||||
:icon="item.fixed ? 'ri:unpin-line' : 'ri:drag-move-2-fill'"
|
||||
@@ -90,7 +97,8 @@
|
||||
:disabled="item.disabled"
|
||||
class="flex-1 min-w-0 [&_.el-checkbox__label]:overflow-hidden [&_.el-checkbox__label]:text-ellipsis [&_.el-checkbox__label]:whitespace-nowrap"
|
||||
>{{
|
||||
item.label || (item.type === 'selection' ? t('table.selection') : '')
|
||||
item.label ||
|
||||
(item.type === 'selection' ? t('table.selection') : '')
|
||||
}}</ElCheckbox
|
||||
>
|
||||
</div>
|
||||
@@ -112,9 +120,12 @@
|
||||
<ElCheckbox v-if="showBorder" v-model="isBorder" :value="true">{{
|
||||
t('table.border')
|
||||
}}</ElCheckbox>
|
||||
<ElCheckbox v-if="showHeaderBackground" v-model="isHeaderBackground" :value="true">{{
|
||||
t('table.headerBackground')
|
||||
}}</ElCheckbox>
|
||||
<ElCheckbox
|
||||
v-if="showHeaderBackground"
|
||||
v-model="isHeaderBackground"
|
||||
:value="true"
|
||||
>{{ t('table.headerBackground') }}</ElCheckbox
|
||||
>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<slot name="right"></slot>
|
||||
|
||||
@@ -91,7 +91,8 @@
|
||||
const paginationRef = ref<HTMLElement>()
|
||||
const tableHeaderRef = ref<HTMLElement>()
|
||||
const tableStore = useTableStore()
|
||||
const { isBorder, isZebra, tableSize, isFullScreen, isHeaderBackground } = storeToRefs(tableStore)
|
||||
const { isBorder, isZebra, tableSize, isFullScreen, isHeaderBackground } =
|
||||
storeToRefs(tableStore)
|
||||
|
||||
/** 分页配置接口 */
|
||||
interface PaginationConfig {
|
||||
|
||||
@@ -118,7 +118,11 @@
|
||||
// 安全计算值
|
||||
const safeTarget = computed(() => validateNumber(props.target, 'target', 0))
|
||||
const safeDuration = computed(() =>
|
||||
clamp(validateNumber(props.duration, 'duration', DEFAULT_DURATION), MIN_DURATION, MAX_DURATION)
|
||||
clamp(
|
||||
validateNumber(props.duration, 'duration', DEFAULT_DURATION),
|
||||
MIN_DURATION,
|
||||
MAX_DURATION
|
||||
)
|
||||
)
|
||||
const safeDecimals = computed(() =>
|
||||
clamp(validateNumber(props.decimals, 'decimals', 0), 0, MAX_DECIMALS)
|
||||
@@ -163,7 +167,12 @@
|
||||
return `${props.prefix}0${props.suffix}`
|
||||
}
|
||||
|
||||
const formattedNumber = formatNumber(value, safeDecimals.value, props.decimal, props.separator)
|
||||
const formattedNumber = formatNumber(
|
||||
value,
|
||||
safeDecimals.value,
|
||||
props.decimal,
|
||||
props.separator
|
||||
)
|
||||
return `${props.prefix}${formattedNumber}${props.suffix}`
|
||||
})
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
:style="{ background: color, '--index': index }"
|
||||
@click="changeThemeColor(color)"
|
||||
>
|
||||
<ArtSvgIcon v-if="color === systemThemeColor" icon="ri:check-fill" class="text-white" />
|
||||
<ArtSvgIcon
|
||||
v-if="color === systemThemeColor"
|
||||
icon="ri:check-fill"
|
||||
class="text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn palette-btn relative z-[2] h-8 w-8 c-p flex-cc tad-300">
|
||||
@@ -44,13 +48,21 @@
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<div v-for="lang in languageOptions" :key="lang.value" class="lang-btn-item">
|
||||
<div
|
||||
v-for="lang in languageOptions"
|
||||
:key="lang.value"
|
||||
class="lang-btn-item"
|
||||
>
|
||||
<ElDropdownItem
|
||||
:command="lang.value"
|
||||
:class="{ 'is-selected': locale === lang.value }"
|
||||
>
|
||||
<span class="menu-txt">{{ lang.label }}</span>
|
||||
<ArtSvgIcon icon="ri:check-fill" class="text-base" v-if="locale === lang.value" />
|
||||
<ArtSvgIcon
|
||||
icon="ri:check-fill"
|
||||
class="text-base"
|
||||
v-if="locale === lang.value"
|
||||
/>
|
||||
</ElDropdownItem>
|
||||
</div>
|
||||
</ElDropdownMenu>
|
||||
|
||||
@@ -18,12 +18,18 @@
|
||||
<!-- 几何装饰元素 -->
|
||||
<div class="geometric-decorations">
|
||||
<!-- 基础几何形状 -->
|
||||
<div class="geo-element circle-outline animate-fade-in-up" style="animation-delay: 0s"></div>
|
||||
<div
|
||||
class="geo-element circle-outline animate-fade-in-up"
|
||||
style="animation-delay: 0s"
|
||||
></div>
|
||||
<div
|
||||
class="geo-element square-rotated animate-fade-in-left"
|
||||
style="animation-delay: 0s"
|
||||
></div>
|
||||
<div class="geo-element circle-small animate-fade-in-up" style="animation-delay: 0.3s"></div>
|
||||
<div
|
||||
class="geo-element circle-small animate-fade-in-up"
|
||||
style="animation-delay: 0.3s"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="geo-element square-bottom-right animate-fade-in-right"
|
||||
@@ -41,7 +47,10 @@
|
||||
></div>
|
||||
|
||||
<!-- 装饰点 -->
|
||||
<div class="geo-element dot dot-top-left animate-bounce-in" style="animation-delay: 0s"></div>
|
||||
<div
|
||||
class="geo-element dot dot-top-left animate-bounce-in"
|
||||
style="animation-delay: 0s"
|
||||
></div>
|
||||
<div
|
||||
class="geo-element dot dot-top-right animate-bounce-in"
|
||||
style="animation-delay: 0s"
|
||||
@@ -475,7 +484,11 @@
|
||||
width: 80px;
|
||||
height: 1px;
|
||||
content: '';
|
||||
background: linear-gradient(90deg, var(--el-color-primary-light-6), transparent);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--el-color-primary-light-6),
|
||||
transparent
|
||||
);
|
||||
opacity: 0;
|
||||
transform: rotate(50deg);
|
||||
animation: lineGrow 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||
|
||||
+5
-1
@@ -77,7 +77,11 @@ const appConfig: SystemConfig = {
|
||||
{ name: 'Left', value: MenuTypeEnum.LEFT, img: configImages.menuLayouts.vertical },
|
||||
{ name: 'Top', value: MenuTypeEnum.TOP, img: configImages.menuLayouts.horizontal },
|
||||
{ name: 'Mixed', value: MenuTypeEnum.TOP_LEFT, img: configImages.menuLayouts.mixed },
|
||||
{ name: 'Dual Column', value: MenuTypeEnum.DUAL_MENU, img: configImages.menuLayouts.dualColumn }
|
||||
{
|
||||
name: 'Dual Column',
|
||||
value: MenuTypeEnum.DUAL_MENU,
|
||||
img: configImages.menuLayouts.dualColumn
|
||||
}
|
||||
],
|
||||
// 菜单主题列表
|
||||
themeList: [
|
||||
|
||||
@@ -100,7 +100,9 @@ export function useCeremony() {
|
||||
*/
|
||||
const currentFestivalData = computed(() => {
|
||||
const currentDate = useDateFormat(new Date(), 'YYYY-MM-DD').value
|
||||
return festivalConfigList.find((item) => isDateInRange(currentDate, item.date, item.endDate))
|
||||
return festivalConfigList.find((item) =>
|
||||
isDateInRange(currentDate, item.date, item.endDate)
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -421,7 +421,9 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
|
||||
}
|
||||
|
||||
// 分页获取数据 (重置到第一页) - 专门用于搜索场景
|
||||
const getDataByPage = async (params?: Partial<TParams>): Promise<ApiResponse<TRecord> | void> => {
|
||||
const getDataByPage = async (
|
||||
params?: Partial<TParams>
|
||||
): Promise<ApiResponse<TRecord> | void> => {
|
||||
pagination.current = 1
|
||||
;(searchParams as Record<string, unknown>)[pageKey] = 1
|
||||
|
||||
|
||||
@@ -73,7 +73,13 @@ export const getColumnChecks = <T>(columns: ColumnOption<T>[]) =>
|
||||
const visibility = getColumnVisibility(col)
|
||||
|
||||
if (special) {
|
||||
return { ...col, prop: special.prop, label: special.label, checked: true, visible: true }
|
||||
return {
|
||||
...col,
|
||||
prop: special.prop,
|
||||
label: special.label,
|
||||
checked: true,
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
return { ...col, checked: visibility, visible: visibility }
|
||||
})
|
||||
@@ -114,7 +120,9 @@ export interface DynamicColumnConfig<T = any> {
|
||||
* @param updates 列更新配置
|
||||
* @deprecated 推荐使用 updateColumn 的数组模式
|
||||
*/
|
||||
batchUpdateColumns: (updates: Array<{ prop: string; updates: Partial<ColumnOption<T>> }>) => void
|
||||
batchUpdateColumns: (
|
||||
updates: Array<{ prop: string; updates: Partial<ColumnOption<T>> }>
|
||||
) => void
|
||||
/**
|
||||
* 重新排序列
|
||||
* @param fromIndex 源索引
|
||||
@@ -156,7 +164,9 @@ export function useTableColumns<T = any>(
|
||||
)
|
||||
const newChecks = getColumnChecks(newCols).map((c) => {
|
||||
const key = getColumnKey(c)
|
||||
const visibility = visibilityMap.has(key) ? visibilityMap.get(key) : getColumnVisibility(c)
|
||||
const visibility = visibilityMap.has(key)
|
||||
? visibilityMap.get(key)
|
||||
: getColumnVisibility(c)
|
||||
return {
|
||||
...c,
|
||||
checked: visibility,
|
||||
@@ -196,7 +206,9 @@ export function useTableColumns<T = any>(
|
||||
const next = [...cols]
|
||||
const columnsToAdd = Array.isArray(column) ? column : [column]
|
||||
const insertIndex =
|
||||
typeof index === 'number' && index >= 0 && index <= next.length ? index : next.length
|
||||
typeof index === 'number' && index >= 0 && index <= next.length
|
||||
? index
|
||||
: next.length
|
||||
|
||||
// 批量插入
|
||||
next.splice(insertIndex, 0, ...columnsToAdd)
|
||||
@@ -302,7 +314,8 @@ export function useTableColumns<T = any>(
|
||||
/**
|
||||
* 获取列配置
|
||||
*/
|
||||
getColumnConfig: (prop: string) => dynamicColumns.value.find((c) => getColumnKey(c) === prop),
|
||||
getColumnConfig: (prop: string) =>
|
||||
dynamicColumns.value.find((c) => getColumnKey(c) === prop),
|
||||
|
||||
/**
|
||||
* 获取所有列配置
|
||||
|
||||
@@ -69,7 +69,10 @@ class TableHeightCalculator {
|
||||
* 获取表格头部高度
|
||||
*/
|
||||
private getHeaderHeight(): number {
|
||||
return this.options.tableHeaderHeight.value || TableHeightCalculator.DEFAULT_TABLE_HEADER_HEIGHT
|
||||
return (
|
||||
this.options.tableHeaderHeight.value ||
|
||||
TableHeightCalculator.DEFAULT_TABLE_HEADER_HEIGHT
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -152,7 +152,10 @@ export function initializeTheme() {
|
||||
setElementThemeColor(settingStore.systemThemeColor)
|
||||
|
||||
// 设置圆角
|
||||
document.documentElement.style.setProperty('--custom-radius', `${settingStore.customRadius}rem`)
|
||||
document.documentElement.style.setProperty(
|
||||
'--custom-radius',
|
||||
`${settingStore.customRadius}rem`
|
||||
)
|
||||
}
|
||||
|
||||
// 应用主题
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 国际化配置
|
||||
*
|
||||
* 基于 vue-i18n 实现的多语言国际化解决方案。
|
||||
* 支持中文和英文切换,自动从本地存储恢复用户的语言偏好。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* - 多语言支持 - 支持中文(简体)和英文两种语言
|
||||
* - 语言切换 - 运行时动态切换语言,无需刷新页面
|
||||
* - 持久化存储 - 自动保存和恢复用户的语言偏好
|
||||
* - 全局注入 - 在任何组件中都可以使用 $t 函数进行翻译
|
||||
* - 类型安全 - 提供 TypeScript 类型支持
|
||||
*
|
||||
* ## 支持的语言
|
||||
*
|
||||
* - zh: 简体中文
|
||||
* - en: English
|
||||
*
|
||||
* @module locales
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { getSystemStorage } from '@/utils/storage'
|
||||
import { StorageKeyManager } from '@/utils/storage/storage-key-manager'
|
||||
|
||||
// 同步导入语言文件
|
||||
import enMessages from './langs/en.json'
|
||||
import zhMessages from './langs/zh.json'
|
||||
|
||||
/**
|
||||
* 存储键管理器实例
|
||||
*/
|
||||
const storageKeyManager = new StorageKeyManager()
|
||||
|
||||
/**
|
||||
* 语言消息对象
|
||||
*/
|
||||
const messages = {
|
||||
'en': enMessages,
|
||||
'zh': zhMessages
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言选项列表
|
||||
* 用于语言切换下拉框
|
||||
*/
|
||||
export const languageOptions = [
|
||||
{ value: 'zh', label: '简体中文' },
|
||||
{ value: 'en', label: 'English' }
|
||||
]
|
||||
|
||||
/**
|
||||
* 从存储中获取语言设置
|
||||
* @returns 语言设置,如果获取失败则返回默认语言
|
||||
*/
|
||||
const getDefaultLanguage = () => {
|
||||
// 尝试从版本化的存储中获取语言设置
|
||||
try {
|
||||
const storageKey = storageKeyManager.getStorageKey('user')
|
||||
const userStore = localStorage.getItem(storageKey)
|
||||
|
||||
if (userStore) {
|
||||
const { language } = JSON.parse(userStore)
|
||||
if (language && Object.values(['zh', 'en']).includes(language)) {
|
||||
return language
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[i18n] 从版本化存储获取语言设置失败:', error)
|
||||
}
|
||||
|
||||
// 尝试从系统存储中获取语言设置
|
||||
try {
|
||||
const sys = getSystemStorage()
|
||||
if (sys) {
|
||||
const { user } = JSON.parse(sys)
|
||||
if (user?.language && Object.values(['zh', 'en']).includes(user.language)) {
|
||||
return user.language
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[i18n] 从系统存储获取语言设置失败:', error)
|
||||
}
|
||||
|
||||
// 返回默认语言
|
||||
console.debug('[i18n] 使用默认语言:', 'zh')
|
||||
return 'zh'
|
||||
}
|
||||
|
||||
/**
|
||||
* i18n 配置选项
|
||||
*/
|
||||
const i18nOptions = {
|
||||
locale: getDefaultLanguage(),
|
||||
legacy: false,
|
||||
globalInjection: true,
|
||||
fallbackLocale: 'zh',
|
||||
messages
|
||||
}
|
||||
|
||||
/**
|
||||
* i18n 实例
|
||||
*/
|
||||
const i18n = createI18n(i18nOptions)
|
||||
|
||||
/**
|
||||
* 全局翻译函数
|
||||
* 可在任何地方使用,无需导入 useI18n
|
||||
*/
|
||||
export const $t = i18n.global.t
|
||||
|
||||
export default i18n
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* 国际化配置
|
||||
*
|
||||
* 基于 vue-i18n 实现的多语言国际化解决方案。
|
||||
* 支持中文和英文切换,自动从本地存储恢复用户的语言偏好。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* - 多语言支持 - 支持中文(简体)和英文两种语言
|
||||
* - 语言切换 - 运行时动态切换语言,无需刷新页面
|
||||
* - 持久化存储 - 自动保存和恢复用户的语言偏好
|
||||
* - 全局注入 - 在任何组件中都可以使用 $t 函数进行翻译
|
||||
* - 类型安全 - 提供 TypeScript 类型支持
|
||||
*
|
||||
* ## 支持的语言
|
||||
*
|
||||
* - zh: 简体中文
|
||||
* - en: English
|
||||
*
|
||||
* @module locales
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import type { I18n, I18nOptions } from 'vue-i18n'
|
||||
import { LanguageEnum } from '@/enums/appEnum'
|
||||
import { getSystemStorage } from '@/utils/storage'
|
||||
import { StorageKeyManager } from '@/utils/storage/storage-key-manager'
|
||||
|
||||
// 同步导入语言文件
|
||||
import enMessages from './langs/en.json'
|
||||
import zhMessages from './langs/zh.json'
|
||||
|
||||
/**
|
||||
* 存储键管理器实例
|
||||
*/
|
||||
const storageKeyManager = new StorageKeyManager()
|
||||
|
||||
/**
|
||||
* 语言消息对象
|
||||
*/
|
||||
const messages = {
|
||||
[LanguageEnum.EN]: enMessages,
|
||||
[LanguageEnum.ZH]: zhMessages
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言选项列表
|
||||
* 用于语言切换下拉框
|
||||
*/
|
||||
export const languageOptions = [
|
||||
{ value: LanguageEnum.ZH, label: '简体中文' },
|
||||
{ value: LanguageEnum.EN, label: 'English' }
|
||||
]
|
||||
|
||||
/**
|
||||
* 从存储中获取语言设置
|
||||
* @returns 语言设置,如果获取失败则返回默认语言
|
||||
*/
|
||||
const getDefaultLanguage = (): LanguageEnum => {
|
||||
// 尝试从版本化的存储中获取语言设置
|
||||
try {
|
||||
const storageKey = storageKeyManager.getStorageKey('user')
|
||||
const userStore = localStorage.getItem(storageKey)
|
||||
|
||||
if (userStore) {
|
||||
const { language } = JSON.parse(userStore)
|
||||
if (language && Object.values(LanguageEnum).includes(language)) {
|
||||
return language
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[i18n] 从版本化存储获取语言设置失败:', error)
|
||||
}
|
||||
|
||||
// 尝试从系统存储中获取语言设置
|
||||
try {
|
||||
const sys = getSystemStorage()
|
||||
if (sys) {
|
||||
const { user } = JSON.parse(sys)
|
||||
if (user?.language && Object.values(LanguageEnum).includes(user.language)) {
|
||||
return user.language
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[i18n] 从系统存储获取语言设置失败:', error)
|
||||
}
|
||||
|
||||
// 返回默认语言
|
||||
console.debug('[i18n] 使用默认语言:', LanguageEnum.ZH)
|
||||
return LanguageEnum.ZH
|
||||
}
|
||||
|
||||
/**
|
||||
* i18n 配置选项
|
||||
*/
|
||||
const i18nOptions: I18nOptions = {
|
||||
locale: getDefaultLanguage(),
|
||||
legacy: false,
|
||||
globalInjection: true,
|
||||
fallbackLocale: LanguageEnum.ZH,
|
||||
messages
|
||||
}
|
||||
|
||||
/**
|
||||
* i18n 实例
|
||||
*/
|
||||
const i18n: I18n = createI18n(i18nOptions)
|
||||
|
||||
/**
|
||||
* 翻译函数类型
|
||||
*/
|
||||
interface Translation {
|
||||
(key: string): string
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局翻译函数
|
||||
* 可在任何地方使用,无需导入 useI18n
|
||||
*/
|
||||
export const $t = i18n.global.t as Translation
|
||||
|
||||
export default i18n
|
||||
@@ -48,20 +48,11 @@
|
||||
"setting": {
|
||||
"menuType": {
|
||||
"title": "Menu Layout",
|
||||
"list": [
|
||||
"Vertical",
|
||||
"Horizontal",
|
||||
"Mixed",
|
||||
"Dual"
|
||||
]
|
||||
"list": ["Vertical", "Horizontal", "Mixed", "Dual"]
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme Style",
|
||||
"list": [
|
||||
"Light",
|
||||
"Dark",
|
||||
"System"
|
||||
]
|
||||
"list": ["Light", "Dark", "System"]
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu Style"
|
||||
@@ -71,17 +62,11 @@
|
||||
},
|
||||
"box": {
|
||||
"title": "Box Style",
|
||||
"list": [
|
||||
"Border",
|
||||
"Shadow"
|
||||
]
|
||||
"list": ["Border", "Shadow"]
|
||||
},
|
||||
"container": {
|
||||
"title": "Container Width",
|
||||
"list": [
|
||||
"Full",
|
||||
"Boxed"
|
||||
]
|
||||
"list": ["Full", "Boxed"]
|
||||
},
|
||||
"basics": {
|
||||
"title": "Basic Config",
|
||||
@@ -127,14 +112,8 @@
|
||||
"notice": {
|
||||
"title": "Notice",
|
||||
"btnRead": "Mark as read",
|
||||
"bar": [
|
||||
"Notice",
|
||||
"Message",
|
||||
"Todo"
|
||||
],
|
||||
"text": [
|
||||
"No"
|
||||
],
|
||||
"bar": ["Notice", "Message", "Todo"],
|
||||
"text": ["No"],
|
||||
"viewAll": "View all"
|
||||
},
|
||||
"worktab": {
|
||||
|
||||
@@ -48,20 +48,11 @@
|
||||
"setting": {
|
||||
"menuType": {
|
||||
"title": "菜单布局",
|
||||
"list": [
|
||||
"垂直",
|
||||
"水平",
|
||||
"混合",
|
||||
"双列"
|
||||
]
|
||||
"list": ["垂直", "水平", "混合", "双列"]
|
||||
},
|
||||
"theme": {
|
||||
"title": "主题风格",
|
||||
"list": [
|
||||
"浅色",
|
||||
"深色",
|
||||
"系统"
|
||||
]
|
||||
"list": ["浅色", "深色", "系统"]
|
||||
},
|
||||
"menu": {
|
||||
"title": "菜单风格"
|
||||
@@ -71,17 +62,11 @@
|
||||
},
|
||||
"box": {
|
||||
"title": "盒子样式",
|
||||
"list": [
|
||||
"边框",
|
||||
"阴影"
|
||||
]
|
||||
"list": ["边框", "阴影"]
|
||||
},
|
||||
"container": {
|
||||
"title": "容器宽度",
|
||||
"list": [
|
||||
"铺满",
|
||||
"定宽"
|
||||
]
|
||||
"list": ["铺满", "定宽"]
|
||||
},
|
||||
"basics": {
|
||||
"title": "基础配置",
|
||||
@@ -127,14 +112,8 @@
|
||||
"notice": {
|
||||
"title": "通知",
|
||||
"btnRead": "标为已读",
|
||||
"bar": [
|
||||
"通知",
|
||||
"消息",
|
||||
"代办"
|
||||
],
|
||||
"text": [
|
||||
"暂无"
|
||||
],
|
||||
"bar": ["通知", "消息", "代办"],
|
||||
"text": ["暂无"],
|
||||
"viewAll": "查看全部"
|
||||
},
|
||||
"worktab": {
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import App from './App.vue'
|
||||
import { createApp } from 'vue'
|
||||
import { initStore } from './store' // Store
|
||||
import { initRouter } from './router' // Router
|
||||
import language from './locales' // 国际化
|
||||
import '@styles/core/tailwind.css' // tailwind
|
||||
import '@styles/index.scss' // 样式
|
||||
import { setupGlobDirectives } from './directives'
|
||||
import { setupErrorHandle } from './utils/sys/error-handle'
|
||||
|
||||
document.addEventListener('touchstart', function () {}, { passive: false })
|
||||
|
||||
const app = createApp(App)
|
||||
initStore(app)
|
||||
initRouter(app)
|
||||
setupGlobDirectives(app)
|
||||
setupErrorHandle(app)
|
||||
|
||||
app.use(language)
|
||||
app.mount('#app')
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import App from './App.vue'
|
||||
import { createApp } from 'vue'
|
||||
import { initStore } from './store' // Store
|
||||
import { initRouter } from './router' // Router
|
||||
import language from './locales' // 国际化
|
||||
import '@styles/core/tailwind.css' // tailwind
|
||||
import '@styles/index.scss' // 样式
|
||||
import '@utils/sys/console.ts' // 控制台输出内容
|
||||
import { setupGlobDirectives } from './directives'
|
||||
import { setupErrorHandle } from './utils/sys/error-handle'
|
||||
|
||||
document.addEventListener(
|
||||
'touchstart',
|
||||
function () {},
|
||||
{ passive: false }
|
||||
)
|
||||
|
||||
const app = createApp(App)
|
||||
initStore(app)
|
||||
initRouter(app)
|
||||
setupGlobDirectives(app)
|
||||
setupErrorHandle(app)
|
||||
|
||||
app.use(language)
|
||||
app.mount('#app')
|
||||
@@ -109,7 +109,11 @@ export class MenuProcessor {
|
||||
}
|
||||
|
||||
// 如果有有效的 component,保留
|
||||
if (item.component && item.component !== '' && item.component !== RoutesAlias.Layout) {
|
||||
if (
|
||||
item.component &&
|
||||
item.component !== '' &&
|
||||
item.component !== RoutesAlias.Layout
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,9 @@ export const useSettingStore = defineStore(
|
||||
* 根据当前日期和节日日期判断是否显示烟花效果
|
||||
*/
|
||||
const isShowFireworks = computed((): boolean => {
|
||||
return festivalDate.value === useCeremony().currentFestivalData.value?.date ? false : true
|
||||
return festivalDate.value === useCeremony().currentFestivalData.value?.date
|
||||
? false
|
||||
: true
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,7 +65,9 @@ export const useWorktabStore = defineStore(
|
||||
const hasOpenedTabs = computed(() => opened.value.length > 0)
|
||||
const hasMultipleTabs = computed(() => opened.value.length > 1)
|
||||
const currentTabIndex = computed(() =>
|
||||
current.value.path ? opened.value.findIndex((tab) => tab.path === current.value.path) : -1
|
||||
current.value.path
|
||||
? opened.value.findIndex((tab) => tab.path === current.value.path)
|
||||
: -1
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -216,7 +218,8 @@ export const useWorktabStore = defineStore(
|
||||
|
||||
// 如果关闭的是当前激活标签,需要激活其他标签
|
||||
if (current.value.path === path) {
|
||||
const newIndex = targetIndex >= opened.value.length ? opened.value.length - 1 : targetIndex
|
||||
const newIndex =
|
||||
targetIndex >= opened.value.length ? opened.value.length - 1 : targetIndex
|
||||
current.value = opened.value[newIndex]
|
||||
safeRouterPush(current.value)
|
||||
}
|
||||
@@ -415,7 +418,8 @@ export const useWorktabStore = defineStore(
|
||||
if (tab.fixedTab) {
|
||||
// 固定标签插入到所有固定标签的末尾
|
||||
const firstNonFixedIndex = opened.value.findIndex((t) => !t.fixedTab)
|
||||
const insertIndex = firstNonFixedIndex === -1 ? opened.value.length : firstNonFixedIndex
|
||||
const insertIndex =
|
||||
firstNonFixedIndex === -1 ? opened.value.length : firstNonFixedIndex
|
||||
opened.value.splice(insertIndex, 0, tab)
|
||||
} else {
|
||||
// 非固定标签插入到所有固定标签后
|
||||
|
||||
Vendored
+4
-1
@@ -109,7 +109,10 @@ declare namespace Api {
|
||||
|
||||
/** 用户搜索参数 */
|
||||
type UserSearchParams = Partial<
|
||||
Pick<UserListItem, 'id' | 'userName' | 'userGender' | 'userPhone' | 'userEmail' | 'status'> &
|
||||
Pick<
|
||||
UserListItem,
|
||||
'id' | 'userName' | 'userGender' | 'userPhone' | 'userEmail' | 'status'
|
||||
> &
|
||||
Api.Common.CommonSearchParams
|
||||
>
|
||||
|
||||
|
||||
@@ -280,9 +280,7 @@ export interface MapChartProps extends BaseChartProps {
|
||||
|
||||
// 双向堆叠柱状图 Props 接口(人口金字塔样式)
|
||||
export interface BidirectionalBarChartProps
|
||||
extends BaseChartProps,
|
||||
AxisDisplayProps,
|
||||
InteractionProps {
|
||||
extends BaseChartProps, AxisDisplayProps, InteractionProps {
|
||||
/** 正向数据(向上显示) */
|
||||
positiveData: number[]
|
||||
/** 负向数据(向下显示) */
|
||||
|
||||
@@ -67,7 +67,11 @@ axiosInstance.interceptors.request.use(
|
||||
const { accessToken } = useUserStore()
|
||||
if (accessToken) request.headers.set('Authorization', accessToken)
|
||||
|
||||
if (request.data && !(request.data instanceof FormData) && !request.headers['Content-Type']) {
|
||||
if (
|
||||
request.data &&
|
||||
!(request.data instanceof FormData) &&
|
||||
!request.headers['Content-Type']
|
||||
) {
|
||||
request.headers.set('Content-Type', 'application/json')
|
||||
request.data = JSON.stringify(request.data)
|
||||
}
|
||||
|
||||
@@ -129,7 +129,10 @@ export default class WebSocketClient {
|
||||
}
|
||||
|
||||
// 发送消息 - 增加消息队列
|
||||
send(data: string | ArrayBufferLike | Blob | ArrayBufferView, immediate: boolean = false): void {
|
||||
send(
|
||||
data: string | ArrayBufferLike | Blob | ArrayBufferView,
|
||||
immediate: boolean = false
|
||||
): void {
|
||||
// 如果要求立即发送且未连接,则直接报错
|
||||
if (immediate && (!this.ws || this.ws.readyState !== WebSocket.OPEN)) {
|
||||
console.error('WebSocket未连接,无法立即发送消息')
|
||||
|
||||
@@ -77,7 +77,9 @@ class StorageCompatibilityManager {
|
||||
const storageKeys = Object.keys(localStorage)
|
||||
const versionPattern = StorageConfig.createVersionPattern()
|
||||
|
||||
return storageKeys.some((key) => versionPattern.test(key) && localStorage.getItem(key) !== null)
|
||||
return storageKeys.some(
|
||||
(key) => versionPattern.test(key) && localStorage.getItem(key) !== null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// ANSI 转义码生成网站 https://patorjk.com/software/taag/#p=display&f=Big&t=ABB%0A
|
||||
const asciiArt = `
|
||||
\x1b[32m欢迎使用 Art Design Pro!
|
||||
\x1b[0m
|
||||
\x1b[36m哇!你居然在用我的项目~ 好用的话别忘了去 GitHub 点个 ★Star 呀,你的支持就是我更新的超强动力!祝使用体验满分💯
|
||||
\x1b[0m
|
||||
\x1b[33mGitHub: https://github.com/Daymychen/art-design-pro
|
||||
\x1b[0m
|
||||
\x1b[31m技术支持(QQ群): 1038930070,和开发者一起交流~ 群里有小伙伴实时答疑,遇到问题不用慌!
|
||||
\x1b[0m
|
||||
`
|
||||
|
||||
console.log(asciiArt)
|
||||
@@ -75,7 +75,9 @@ export function registerResourceErrorHandler() {
|
||||
const target = event.target as HTMLElement
|
||||
if (
|
||||
target &&
|
||||
(target.tagName === 'IMG' || target.tagName === 'SCRIPT' || target.tagName === 'LINK')
|
||||
(target.tagName === 'IMG' ||
|
||||
target.tagName === 'SCRIPT' ||
|
||||
target.tagName === 'LINK')
|
||||
) {
|
||||
console.error('[ResourceError]', {
|
||||
tagName: target.tagName,
|
||||
|
||||
@@ -97,7 +97,9 @@ class VersionManager {
|
||||
const oldSysKey =
|
||||
storageKeys.find(
|
||||
(key) =>
|
||||
StorageConfig.isVersionedKey(key) && key !== currentVersionPrefix && !key.includes('-')
|
||||
StorageConfig.isVersionedKey(key) &&
|
||||
key !== currentVersionPrefix &&
|
||||
!key.includes('-')
|
||||
) || null
|
||||
|
||||
// 查找旧版本的分离存储键
|
||||
@@ -121,7 +123,9 @@ class VersionManager {
|
||||
return upgradeLogList.value.some((item) => {
|
||||
const itemVersion = this.normalizeVersion(item.version)
|
||||
return (
|
||||
item.requireReLogin && itemVersion > normalizedStored && itemVersion <= normalizedCurrent
|
||||
item.requireReLogin &&
|
||||
itemVersion > normalizedStored &&
|
||||
itemVersion <= normalizedCurrent
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,21 +11,13 @@
|
||||
<p class="sub-title">{{ $t('forgetPassword.subTitle') }}</p>
|
||||
<div class="mt-5">
|
||||
<span class="input-label" v-if="showInputLabel">账号</span>
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
:placeholder="$t('forgetPassword.placeholder')"
|
||||
v-model.trim="username"
|
||||
/>
|
||||
<ElInput class="custom-height" :placeholder="$t('forgetPassword.placeholder')"
|
||||
v-model.trim="username" />
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton
|
||||
class="w-full custom-height"
|
||||
type="primary"
|
||||
@click="register"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
<ElButton class="w-full custom-height" type="primary" @click="register" :loading="loading"
|
||||
v-ripple>
|
||||
{{ $t('forgetPassword.submitBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -41,22 +33,22 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'ForgetPassword' })
|
||||
<script setup>
|
||||
defineOptions({ name: 'ForgetPassword' })
|
||||
|
||||
const router = useRouter()
|
||||
const showInputLabel = ref(false)
|
||||
const router = useRouter()
|
||||
const showInputLabel = ref(false)
|
||||
|
||||
const username = ref('')
|
||||
const loading = ref(false)
|
||||
const username = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const register = async () => {}
|
||||
const register = async () => { }
|
||||
|
||||
const toLogin = () => {
|
||||
const toLogin = () => {
|
||||
router.push({ name: 'Login' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '../login/style.css';
|
||||
@import '../login/style.css';
|
||||
</style>
|
||||
|
||||
+68
-112
@@ -10,65 +10,36 @@
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('login.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('login.subTitle') }}</p>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:key="formKey"
|
||||
@keyup.enter="handleSubmit"
|
||||
style="margin-top: 25px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" :key="formKey" @keyup.enter="handleSubmit"
|
||||
style="margin-top: 25px">
|
||||
<ElFormItem prop="account">
|
||||
<ElSelect v-model="formData.account" @change="setupAccount">
|
||||
<ElOption
|
||||
v-for="account in accounts"
|
||||
:key="account.key"
|
||||
:label="account.label"
|
||||
:value="account.key"
|
||||
>
|
||||
<ElOption v-for="account in accounts" :key="account.key" :label="account.label"
|
||||
:value="account.key">
|
||||
<span>{{ account.label }}</span>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="username">
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
:placeholder="$t('login.placeholder.username')"
|
||||
v-model.trim="formData.username"
|
||||
/>
|
||||
<ElInput class="custom-height" :placeholder="$t('login.placeholder.username')"
|
||||
v-model.trim="formData.username" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
:placeholder="$t('login.placeholder.password')"
|
||||
v-model.trim="formData.password"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
show-password
|
||||
/>
|
||||
<ElInput class="custom-height" :placeholder="$t('login.placeholder.password')"
|
||||
v-model.trim="formData.password" type="password" autocomplete="off" show-password />
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 推拽验证 -->
|
||||
<div class="relative pb-5 mt-6">
|
||||
<div
|
||||
class="relative z-[2] overflow-hidden select-none rounded-lg border border-transparent tad-300"
|
||||
:class="{ '!border-[#FF4E4F]': !isPassing && isClickPass }"
|
||||
>
|
||||
<ArtDragVerify
|
||||
ref="dragVerify"
|
||||
v-model:value="isPassing"
|
||||
:text="$t('login.sliderText')"
|
||||
textColor="var(--art-gray-700)"
|
||||
:successText="$t('login.sliderSuccessText')"
|
||||
progressBarBg="var(--main-color)"
|
||||
:background="isDark ? '#26272F' : '#F1F1F4'"
|
||||
handlerBg="var(--default-box-color)"
|
||||
/>
|
||||
<div class="relative z-[2] overflow-hidden select-none rounded-lg border border-transparent tad-300"
|
||||
:class="{ '!border-[#FF4E4F]': !isPassing && isClickPass }">
|
||||
<ArtDragVerify ref="dragVerify" v-model:value="isPassing" :text="$t('login.sliderText')"
|
||||
textColor="var(--art-gray-700)" :successText="$t('login.sliderSuccessText')"
|
||||
progressBarBg="var(--main-color)" :background="isDark ? '#26272F' : '#F1F1F4'"
|
||||
handlerBg="var(--default-box-color)" />
|
||||
</div>
|
||||
<p
|
||||
class="absolute top-0 z-[1] px-px mt-2 text-xs text-[#f56c6c] tad-300"
|
||||
:class="{ 'translate-y-10': !isPassing && isClickPass }"
|
||||
>
|
||||
<p class="absolute top-0 z-[1] px-px mt-2 text-xs text-[#f56c6c] tad-300"
|
||||
:class="{ 'translate-y-10': !isPassing && isClickPass }">
|
||||
{{ $t('login.placeholder.slider') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -83,13 +54,8 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px">
|
||||
<ElButton
|
||||
class="w-full custom-height"
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
<ElButton class="w-full custom-height" type="primary" @click="handleSubmit"
|
||||
:loading="loading" v-ripple>
|
||||
{{ $t('login.btnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -107,38 +73,28 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { HttpError } from '@/utils/http/error'
|
||||
import { fetchLogin } from '@/api/auth'
|
||||
import { ElNotification, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
<script setup>
|
||||
import AppConfig from '@/config'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { HttpError } from '@/utils/http/error'
|
||||
import { fetchLogin } from '@/api/auth'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
|
||||
defineOptions({ name: 'Login' })
|
||||
defineOptions({ name: 'Login' })
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const { isDark } = storeToRefs(settingStore)
|
||||
const { t, locale } = useI18n()
|
||||
const formKey = ref(0)
|
||||
const settingStore = useSettingStore()
|
||||
const { isDark } = storeToRefs(settingStore)
|
||||
const { t, locale } = useI18n()
|
||||
const formKey = ref(0)
|
||||
|
||||
// 监听语言切换,重置表单
|
||||
watch(locale, () => {
|
||||
// 监听语言切换,重置表单
|
||||
watch(locale, () => {
|
||||
formKey.value++
|
||||
})
|
||||
})
|
||||
|
||||
type AccountKey = 'super' | 'admin' | 'user'
|
||||
|
||||
export interface Account {
|
||||
key: AccountKey
|
||||
label: string
|
||||
userName: string
|
||||
password: string
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
const accounts = computed<Account[]>(() => [
|
||||
const accounts = computed(() => [
|
||||
{
|
||||
key: 'super',
|
||||
label: t('login.roles.super'),
|
||||
@@ -160,47 +116,47 @@
|
||||
password: '123456',
|
||||
roles: ['R_USER']
|
||||
}
|
||||
])
|
||||
])
|
||||
|
||||
const dragVerify = ref()
|
||||
const dragVerify = ref()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const isPassing = ref(false)
|
||||
const isClickPass = ref(false)
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const isPassing = ref(false)
|
||||
const isClickPass = ref(false)
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
const formRef = ref<FormInstance>()
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
const formRef = ref()
|
||||
|
||||
const formData = reactive({
|
||||
const formData = reactive({
|
||||
account: '',
|
||||
username: '',
|
||||
password: '',
|
||||
rememberPassword: true
|
||||
})
|
||||
})
|
||||
|
||||
const rules = computed<FormRules>(() => ({
|
||||
const rules = computed(() => ({
|
||||
username: [{ required: true, message: t('login.placeholder.username'), trigger: 'blur' }],
|
||||
password: [{ required: true, message: t('login.placeholder.password'), trigger: 'blur' }]
|
||||
}))
|
||||
}))
|
||||
|
||||
const loading = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(() => {
|
||||
setupAccount('super')
|
||||
})
|
||||
})
|
||||
|
||||
// 设置账号
|
||||
const setupAccount = (key: AccountKey) => {
|
||||
const selectedAccount = accounts.value.find((account: Account) => account.key === key)
|
||||
// 设置账号
|
||||
const setupAccount = (key) => {
|
||||
const selectedAccount = accounts.value.find((account) => account.key === key)
|
||||
formData.account = key
|
||||
formData.username = selectedAccount?.userName ?? ''
|
||||
formData.password = selectedAccount?.password ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
// 登录
|
||||
const handleSubmit = async () => {
|
||||
// 登录
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
@@ -237,7 +193,7 @@
|
||||
showLoginSuccessNotice()
|
||||
|
||||
// 获取 redirect 参数,如果存在则跳转到指定页面,否则跳转到首页
|
||||
const redirect = route.query.redirect as string
|
||||
const redirect = route.query.redirect
|
||||
router.push(redirect || '/')
|
||||
} catch (error) {
|
||||
// 处理 HttpError
|
||||
@@ -252,15 +208,15 @@
|
||||
loading.value = false
|
||||
resetDragVerify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置拖拽验证
|
||||
const resetDragVerify = () => {
|
||||
// 重置拖拽验证
|
||||
const resetDragVerify = () => {
|
||||
dragVerify.value.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// 登录成功提示
|
||||
const showLoginSuccessNotice = () => {
|
||||
// 登录成功提示
|
||||
const showLoginSuccessNotice = () => {
|
||||
setTimeout(() => {
|
||||
ElNotification({
|
||||
title: t('login.success.title'),
|
||||
@@ -270,15 +226,15 @@
|
||||
message: `${t('login.success.message')}, ${systemName}!`
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import './style.css';
|
||||
@import './style.css';
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-select__wrapper) {
|
||||
:deep(.el-select__wrapper) {
|
||||
height: 40px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,64 +10,36 @@
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('register.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('register.subTitle') }}</p>
|
||||
<ElForm
|
||||
class="mt-7.5"
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
:key="formKey"
|
||||
>
|
||||
<ElForm class="mt-7.5" ref="formRef" :model="formData" :rules="rules" label-position="top"
|
||||
:key="formKey">
|
||||
<ElFormItem prop="username">
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
v-model.trim="formData.username"
|
||||
:placeholder="$t('register.placeholder.username')"
|
||||
/>
|
||||
<ElInput class="custom-height" v-model.trim="formData.username"
|
||||
:placeholder="$t('register.placeholder.username')" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
v-model.trim="formData.password"
|
||||
:placeholder="$t('register.placeholder.password')"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
show-password
|
||||
/>
|
||||
<ElInput class="custom-height" v-model.trim="formData.password"
|
||||
:placeholder="$t('register.placeholder.password')" type="password" autocomplete="off"
|
||||
show-password />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="confirmPassword">
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
v-model.trim="formData.confirmPassword"
|
||||
:placeholder="$t('register.placeholder.confirmPassword')"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
@keyup.enter="register"
|
||||
show-password
|
||||
/>
|
||||
<ElInput class="custom-height" v-model.trim="formData.confirmPassword"
|
||||
:placeholder="$t('register.placeholder.confirmPassword')" type="password"
|
||||
autocomplete="off" @keyup.enter="register" show-password />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="agreement">
|
||||
<ElCheckbox v-model="formData.agreement">
|
||||
{{ $t('register.agreeText') }}
|
||||
<RouterLink
|
||||
style="color: var(--theme-color); text-decoration: none"
|
||||
to="/privacy-policy"
|
||||
>{{ $t('register.privacyPolicy') }}</RouterLink
|
||||
>
|
||||
<RouterLink style="color: var(--theme-color); text-decoration: none"
|
||||
to="/privacy-policy">{{ $t('register.privacyPolicy') }}</RouterLink>
|
||||
</ElCheckbox>
|
||||
</ElFormItem>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton
|
||||
class="w-full custom-height"
|
||||
type="primary"
|
||||
@click="register"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
<ElButton class="w-full custom-height" type="primary" @click="register" :loading="loading"
|
||||
v-ripple>
|
||||
{{ $t('register.submitBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -85,48 +57,40 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineOptions({ name: 'Register' })
|
||||
defineOptions({ name: 'Register' })
|
||||
|
||||
interface RegisterForm {
|
||||
username: string
|
||||
password: string
|
||||
confirmPassword: string
|
||||
agreement: boolean
|
||||
}
|
||||
const USERNAME_MIN_LENGTH = 3
|
||||
const USERNAME_MAX_LENGTH = 20
|
||||
const PASSWORD_MIN_LENGTH = 6
|
||||
const REDIRECT_DELAY = 1000
|
||||
|
||||
const USERNAME_MIN_LENGTH = 3
|
||||
const USERNAME_MAX_LENGTH = 20
|
||||
const PASSWORD_MIN_LENGTH = 6
|
||||
const REDIRECT_DELAY = 1000
|
||||
const { t, locale } = useI18n()
|
||||
const router = useRouter()
|
||||
const formRef = ref()
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const router = useRouter()
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
const formKey = ref(0)
|
||||
|
||||
const loading = ref(false)
|
||||
const formKey = ref(0)
|
||||
|
||||
// 监听语言切换,重置表单
|
||||
watch(locale, () => {
|
||||
// 监听语言切换,重置表单
|
||||
watch(locale, () => {
|
||||
formKey.value++
|
||||
})
|
||||
})
|
||||
|
||||
const formData = reactive<RegisterForm>({
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
agreement: false
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
/**
|
||||
* 验证密码
|
||||
* 当密码输入后,如果确认密码已填写,则触发确认密码的验证
|
||||
*/
|
||||
const validatePassword = (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
const validatePassword = (_rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback(new Error(t('register.placeholder.password')))
|
||||
return
|
||||
@@ -137,17 +101,17 @@
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 验证确认密码
|
||||
* 检查确认密码是否与密码一致
|
||||
*/
|
||||
const validateConfirmPassword = (
|
||||
_rule: any,
|
||||
value: string,
|
||||
callback: (error?: Error) => void
|
||||
) => {
|
||||
const validateConfirmPassword = (
|
||||
_rule,
|
||||
value,
|
||||
callback
|
||||
) => {
|
||||
if (!value) {
|
||||
callback(new Error(t('register.rule.confirmPasswordRequired')))
|
||||
return
|
||||
@@ -159,21 +123,21 @@
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 验证用户协议
|
||||
* 确保用户已勾选同意协议
|
||||
*/
|
||||
const validateAgreement = (_rule: any, value: boolean, callback: (error?: Error) => void) => {
|
||||
const validateAgreement = (_rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback(new Error(t('register.rule.agreementRequired')))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = computed<FormRules<RegisterForm>>(() => ({
|
||||
const rules = computed(() => ({
|
||||
username: [
|
||||
{ required: true, message: t('register.placeholder.username'), trigger: 'blur' },
|
||||
{
|
||||
@@ -185,17 +149,21 @@
|
||||
],
|
||||
password: [
|
||||
{ required: true, validator: validatePassword, trigger: 'blur' },
|
||||
{ min: PASSWORD_MIN_LENGTH, message: t('register.rule.passwordLength'), trigger: 'blur' }
|
||||
{
|
||||
min: PASSWORD_MIN_LENGTH,
|
||||
message: t('register.rule.passwordLength'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }],
|
||||
agreement: [{ validator: validateAgreement, trigger: 'change' }]
|
||||
}))
|
||||
}))
|
||||
|
||||
/**
|
||||
/**
|
||||
* 注册用户
|
||||
* 验证表单后提交注册请求
|
||||
*/
|
||||
const register = async () => {
|
||||
const register = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
@@ -223,18 +191,18 @@
|
||||
console.error('表单验证失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 跳转到登录页面
|
||||
*/
|
||||
const toLogin = () => {
|
||||
const toLogin = () => {
|
||||
setTimeout(() => {
|
||||
router.push({ name: 'Login' })
|
||||
}, REDIRECT_DELAY)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '../login/style.css';
|
||||
@import '../login/style.css';
|
||||
</style>
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CardList from './modules/card-list.vue'
|
||||
import ActiveUser from './modules/active-user.vue'
|
||||
import SalesOverview from './modules/sales-overview.vue'
|
||||
import NewUser from './modules/new-user.vue'
|
||||
import Dynamic from './modules/dynamic-stats.vue'
|
||||
import TodoList from './modules/todo-list.vue'
|
||||
import AboutProject from './modules/about-project.vue'
|
||||
<script setup>
|
||||
import CardList from './modules/card-list.vue'
|
||||
import ActiveUser from './modules/active-user.vue'
|
||||
import SalesOverview from './modules/sales-overview.vue'
|
||||
import NewUser from './modules/new-user.vue'
|
||||
import Dynamic from './modules/dynamic-stats.vue'
|
||||
import TodoList from './modules/todo-list.vue'
|
||||
import AboutProject from './modules/about-project.vue'
|
||||
|
||||
defineOptions({ name: 'Console' })
|
||||
defineOptions({ name: 'Console' })
|
||||
</script>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup>
|
||||
import AppConfig from '@/config'
|
||||
import { WEB_LINKS } from '@/utils/constants'
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
* 在新标签页中打开指定 URL
|
||||
* @param url 要打开的网页地址
|
||||
*/
|
||||
const goPage = (url: string): void => {
|
||||
const goPage = (url) => {
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
<div class="ml-1">
|
||||
<h3 class="mt-5 text-lg font-medium">用户概述</h3>
|
||||
<p class="mt-1 text-sm">比上周 <span class="text-success font-medium">+23%</span></p>
|
||||
<p class="mt-1 text-sm">我们为您创建了多个选项,可将它们组合在一起并定制为像素完美的页面</p>
|
||||
<p class="mt-1 text-sm"
|
||||
>我们为您创建了多个选项,可将它们组合在一起并定制为像素完美的页面</p
|
||||
>
|
||||
</div>
|
||||
<div class="flex-b mt-2">
|
||||
<div class="flex-1" v-for="(item, index) in list" :key="index">
|
||||
@@ -22,11 +24,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface UserStatItem {
|
||||
name: string
|
||||
num: string
|
||||
}
|
||||
<script setup>
|
||||
|
||||
// 最近9个月
|
||||
const xAxisLabels = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月']
|
||||
@@ -38,7 +36,7 @@
|
||||
* 用户统计数据列表
|
||||
* 包含总用户量、总访问量、日访问量和周同比等关键指标
|
||||
*/
|
||||
const list: UserStatItem[] = [
|
||||
const list = [
|
||||
{ name: '总用户量', num: '32k' },
|
||||
{ name: '总访问量', num: '128k' },
|
||||
{ name: '日访问量', num: '1.2k' },
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
<ElCol v-for="(item, index) in dataList" :key="index" :sm="12" :md="6" :lg="6">
|
||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
||||
<span class="text-g-700 text-sm">{{ item.des }}</span>
|
||||
<ArtCountTo class="text-[26px] font-medium mt-2" :target="item.num" :duration="1300" />
|
||||
<ArtCountTo
|
||||
class="text-[26px] font-medium mt-2"
|
||||
:target="item.num"
|
||||
:duration="1300"
|
||||
/>
|
||||
<div class="flex-c mt-1">
|
||||
<span class="text-xs text-g-600">较上周</span>
|
||||
<span
|
||||
@@ -23,21 +27,13 @@
|
||||
</ElRow>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface CardDataItem {
|
||||
des: string
|
||||
icon: string
|
||||
startVal: number
|
||||
duration: number
|
||||
num: number
|
||||
change: string
|
||||
}
|
||||
<script setup>
|
||||
|
||||
/**
|
||||
* 卡片统计数据列表
|
||||
* 展示总访问次数、在线访客数、点击量和新用户等核心数据指标
|
||||
*/
|
||||
const dataList = reactive<CardDataItem[]>([
|
||||
const dataList = reactive([
|
||||
{
|
||||
des: '总访问次数',
|
||||
icon: 'ri:pie-chart-line',
|
||||
|
||||
@@ -23,18 +23,12 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface DynamicItem {
|
||||
username: string
|
||||
type: string
|
||||
target: string
|
||||
}
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 用户动态列表
|
||||
* 记录用户的关注、发文、提问、兑换等各类活动
|
||||
*/
|
||||
const list = reactive<DynamicItem[]>([
|
||||
const list = reactive([
|
||||
{
|
||||
username: '中小鱼',
|
||||
type: '关注了',
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
<ElTableColumn label="性别" prop="avatar">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span style="margin-left: 10px">{{ scope.row.sex === 1 ? '男' : '女' }}</span>
|
||||
<span style="margin-left: 10px">{{
|
||||
scope.row.sex === 1 ? '男' : '女'
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
@@ -52,7 +54,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup>
|
||||
import avatar1 from '@/assets/images/avatar/avatar1.webp'
|
||||
import avatar2 from '@/assets/images/avatar/avatar2.webp'
|
||||
import avatar3 from '@/assets/images/avatar/avatar3.webp'
|
||||
@@ -60,16 +62,6 @@
|
||||
import avatar5 from '@/assets/images/avatar/avatar5.webp'
|
||||
import avatar6 from '@/assets/images/avatar/avatar6.webp'
|
||||
|
||||
interface UserTableItem {
|
||||
username: string
|
||||
province: string
|
||||
sex: 0 | 1
|
||||
age: number
|
||||
percentage: number
|
||||
pro: number
|
||||
color: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
const ANIMATION_DELAY = 100
|
||||
|
||||
@@ -79,7 +71,7 @@
|
||||
* 新用户表格数据
|
||||
* 包含用户基本信息和完成进度
|
||||
*/
|
||||
const tableData = reactive<UserTableItem[]>([
|
||||
const tableData = reactive([
|
||||
{
|
||||
username: '中小鱼',
|
||||
province: '北京',
|
||||
@@ -146,7 +138,7 @@
|
||||
* 添加进度条动画效果
|
||||
* 延迟后将进度值从 0 更新到目标百分比,触发动画
|
||||
*/
|
||||
const addAnimation = (): void => {
|
||||
const addAnimation = () => {
|
||||
setTimeout(() => {
|
||||
tableData.forEach((item) => {
|
||||
item.pro = item.percentage
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup>
|
||||
/**
|
||||
* 全年访问量数据
|
||||
* 记录每月的访问量统计
|
||||
|
||||
@@ -25,18 +25,12 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface TodoItem {
|
||||
username: string
|
||||
date: string
|
||||
complate: boolean
|
||||
}
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 待办事项列表
|
||||
* 记录每日工作任务及完成状态
|
||||
*/
|
||||
const list = reactive<TodoItem[]>([
|
||||
const list = reactive([
|
||||
{
|
||||
username: '查看今天工作内容',
|
||||
date: '上午 09:30',
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup>
|
||||
import imgUrl from '@imgs/svg/403.svg'
|
||||
defineOptions({ name: 'Exception403' })
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup>
|
||||
import imgUrl from '@imgs/svg/404.svg'
|
||||
defineOptions({ name: 'Exception404' })
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup>
|
||||
import imgUrl from '@imgs/svg/500.svg'
|
||||
defineOptions({ name: 'Exception500' })
|
||||
</script>
|
||||
|
||||
@@ -20,10 +20,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'AppLayout' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup>
|
||||
import { IframeRouteManager } from '@/router/core'
|
||||
|
||||
defineOptions({ name: 'IframeView' })
|
||||
@@ -18,7 +18,7 @@
|
||||
const route = useRoute()
|
||||
const isLoading = ref(true)
|
||||
const iframeUrl = ref('')
|
||||
const iframeRef = ref<HTMLIFrameElement | null>(null)
|
||||
const iframeRef = ref(null)
|
||||
|
||||
/**
|
||||
* 初始化 iframe URL
|
||||
@@ -36,7 +36,7 @@
|
||||
* 处理 iframe 加载完成事件
|
||||
* 隐藏加载状态
|
||||
*/
|
||||
const handleIframeLoad = (): void => {
|
||||
const handleIframeLoad = () => {
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,6 +23,6 @@
|
||||
</ArtResultPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup>
|
||||
defineOptions({ name: 'ResultFail' })
|
||||
</script>
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
</ArtResultPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup>
|
||||
defineOptions({ name: 'ResultSuccess' })
|
||||
</script>
|
||||
|
||||
@@ -170,7 +170,12 @@
|
||||
if (form.menuType === 'menu') {
|
||||
return [
|
||||
...baseItems,
|
||||
{ label: '菜单名称', key: 'name', type: 'input', props: { placeholder: '菜单名称' } },
|
||||
{
|
||||
label: '菜单名称',
|
||||
key: 'name',
|
||||
type: 'input',
|
||||
props: { placeholder: '菜单名称' }
|
||||
},
|
||||
{
|
||||
label: createLabelTooltip(
|
||||
'路由地址',
|
||||
@@ -180,7 +185,12 @@
|
||||
type: 'input',
|
||||
props: { placeholder: '如:/dashboard 或 console' }
|
||||
},
|
||||
{ label: '权限标识', key: 'label', type: 'input', props: { placeholder: '如:User' } },
|
||||
{
|
||||
label: '权限标识',
|
||||
key: 'label',
|
||||
type: 'input',
|
||||
props: { placeholder: '如:User' }
|
||||
},
|
||||
{
|
||||
label: createLabelTooltip(
|
||||
'组件路径',
|
||||
@@ -190,7 +200,12 @@
|
||||
type: 'input',
|
||||
props: { placeholder: '如:/system/user 或留空' }
|
||||
},
|
||||
{ label: '图标', key: 'icon', type: 'input', props: { placeholder: '如:ri:user-line' } },
|
||||
{
|
||||
label: '图标',
|
||||
key: 'icon',
|
||||
type: 'input',
|
||||
props: { placeholder: '如:ri:user-line' }
|
||||
},
|
||||
{
|
||||
label: createLabelTooltip(
|
||||
'角色权限',
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
<template #footer>
|
||||
<ElButton @click="outputSelectedData" style="margin-left: 8px">获取选中数据</ElButton>
|
||||
|
||||
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
|
||||
<ElButton @click="toggleExpandAll">{{
|
||||
isExpandAll ? '全部收起' : '全部展开'
|
||||
}}</ElButton>
|
||||
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
|
||||
isSelectAll ? '取消全选' : '全部选择'
|
||||
}}</ElButton>
|
||||
@@ -114,7 +116,9 @@
|
||||
checked: auth.checked || false
|
||||
}))
|
||||
|
||||
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
|
||||
processed.children = processed.children
|
||||
? [...processed.children, ...authNodes]
|
||||
: authNodes
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
<div class="relative flex-b mt-2.5 max-md:block max-md:mt-1">
|
||||
<div class="w-112 mr-5 max-md:w-full max-md:mr-0">
|
||||
<div class="art-card-sm relative p-9 pb-6 overflow-hidden text-center">
|
||||
<img class="absolute top-0 left-0 w-full h-50 object-cover" src="@imgs/user/bg.webp" />
|
||||
<img
|
||||
class="absolute top-0 left-0 w-full h-50 object-cover"
|
||||
src="@imgs/user/bg.webp"
|
||||
/>
|
||||
<img
|
||||
class="relative z-10 w-20 h-20 mt-30 mx-auto object-cover border-2 border-white rounded-full"
|
||||
src="@imgs/user/avatar.webp"
|
||||
@@ -62,7 +65,11 @@
|
||||
<ElInput v-model="form.realName" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="性别" prop="sex" class="ml-5">
|
||||
<ElSelect v-model="form.sex" placeholder="Select" :disabled="!isEdit">
|
||||
<ElSelect
|
||||
v-model="form.sex"
|
||||
placeholder="Select"
|
||||
:disabled="!isEdit"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
@@ -92,7 +99,12 @@
|
||||
</ElRow>
|
||||
|
||||
<ElFormItem label="个人介绍" prop="des" class="h-32">
|
||||
<ElInput type="textarea" :rows="4" v-model="form.des" :disabled="!isEdit" />
|
||||
<ElInput
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
v-model="form.des"
|
||||
:disabled="!isEdit"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<div class="flex-c justify-end [&_.el-button]:!w-27.5">
|
||||
@@ -106,7 +118,12 @@
|
||||
<div class="art-card-sm my-5">
|
||||
<h1 class="p-4 text-xl font-normal border-b border-g-300">更改密码</h1>
|
||||
|
||||
<ElForm :model="pwdForm" class="box-border p-5" label-width="86px" label-position="top">
|
||||
<ElForm
|
||||
:model="pwdForm"
|
||||
class="box-border p-5"
|
||||
label-width="86px"
|
||||
label-position="top"
|
||||
>
|
||||
<ElFormItem label="当前密码" prop="password">
|
||||
<ElInput
|
||||
v-model="pwdForm.password"
|
||||
|
||||
@@ -6,11 +6,19 @@
|
||||
<template>
|
||||
<div class="user-page art-full-height">
|
||||
<!-- 搜索栏 -->
|
||||
<UserSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams"></UserSearch>
|
||||
<UserSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></UserSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<ArtTableHeader
|
||||
v-model:columns="columnChecks"
|
||||
:loading="loading"
|
||||
@refresh="refreshData"
|
||||
>
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton @click="showDialog('add')" v-ripple>新增用户</ElButton>
|
||||
|
||||
Reference in New Issue
Block a user