This commit is contained in:
Your Name
2026-07-06 15:00:10 +08:00
parent eb0efba739
commit e76f16193b
889 changed files with 4758 additions and 3046 deletions
+1 -1
View File
@@ -29,7 +29,7 @@
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-width: 2;
stroke: #4073fa;
stroke: #06b6d4;
stroke-linecap: round;
}
+158
View File
@@ -0,0 +1,158 @@
/**
* 将标准双 el-card 列表页迁移为 admin-page 架构组件
* 用法: node scripts/migrate-admin-pages.mjs [--dry-run] [glob...]
*/
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const viewsRoot = path.join(__dirname, '../src/views')
const dryRun = process.argv.includes('--dry-run')
const extraPaths = process.argv.slice(2).filter((a) => !a.startsWith('--'))
const defaultTargets = [
'article/lists/index.vue',
'article/column/index.vue',
'permission/role/index.vue',
'permission/menu/index.vue',
'dev_tools/code/index.vue',
'setting/dict/type/index.vue',
'setting/dict/data/index.vue',
'message/notice/index.vue',
'message/short_letter/index.vue',
'fans/index.vue',
'order/index.vue',
'asset/user/index.vue',
'asset/resource/index.vue',
'finance/balance_details.vue',
'finance/recharge_record.vue',
'finance/refund_record.vue'
]
function migrate(content) {
if (content.includes('admin-page-filter-panel')) return null
if (!content.includes('el-card class="!border-none"')) return null
if (!content.includes('<el-table')) return null
let out = content
// 根容器 class 清理
out = out.replace(
/<div class="[^"]*">\s*\n\s*<el-card class="!border-none" shadow="never">/,
'<div>\n <admin-page-filter-panel>'
)
if (!out.includes('admin-page-filter-panel')) {
out = out.replace(
/<el-card class="!border-none" shadow="never">/,
'<admin-page-filter-panel>'
)
}
// 第一个 filter card 结束
out = out.replace(
/<\/el-form>\s*\n\s*<\/el-card>/,
'</el-form>\n </admin-page-filter-panel>'
)
// 第二个 data card 开始 - 提取 v-loading
const loadingMatch = out.match(
/<el-card([^>]*?)class="[^"]*mt-4[^"]*"[^>]*shadow="never"[^>]*>/
)
const altLoadingMatch = out.match(
/<el-card v-loading="([^"]+)"([^>]*)class="[^"]*mt-4[^"]*"([^>]*)shadow="never"([^>]*)>/
)
if (altLoadingMatch) {
out = out.replace(
altLoadingMatch[0],
`<admin-page-data-panel v-loading="${altLoadingMatch[1]}">`
)
} else if (loadingMatch) {
out = out.replace(loadingMatch[0], '<admin-page-data-panel>')
} else {
out = out.replace(
/<el-card class="!border-none mt-4" shadow="never">/,
'<admin-page-data-panel>'
)
out = out.replace(
/<el-card v-loading="([^"]+)" class="mt-4 !border-none" shadow="never">/,
'<admin-page-data-panel v-loading="$1">'
)
out = out.replace(
/<el-card v-loading="([^"]+)" class="!border-none mt-4" shadow="never">/,
'<admin-page-data-panel v-loading="$1">'
)
}
// toolbar: 紧跟 data panel 后的首个 div 包裹按钮
out = out.replace(
/(<admin-page-data-panel[^>]*>\s*)<div>\s*\n(\s*<el-button[\s\S]*?<\/div>\s*\n)/,
'$1<template #toolbar>\n$2</template>\n'
)
out = out.replace(
/(<admin-page-data-panel[^>]*>\s*)<div>\s*\n(\s*<router-link[\s\S]*?<\/div>\s*\n)/,
'$1<template #toolbar>\n$2</template>\n'
)
// 移除 table 前的 mt-4 class
out = out.replace(/<el-table class="mt-4"/g, '<el-table')
out = out.replace(/<el-table\s+class="mt-4"\s+/g, '<el-table ')
// pagination footer
out = out.replace(
/<div class="flex(?: mt-4)? justify-end(?: mt-4)?">\s*\n\s*<pagination([^/]*)\/>\s*\n\s*<\/div>\s*\n\s*<\/el-card>/,
'<template #footer>\n <pagination$1/>\n </template>\n </admin-page-data-panel>'
)
out = out.replace(
/<div class="flex mt-4 justify-end">\s*\n\s*<pagination([^/]*)\/>\s*\n\s*<\/div>\s*\n\s*<\/el-card>/,
'<template #footer>\n <pagination$1/>\n </template>\n </admin-page-data-panel>'
)
// 无 pagination 的 data card 闭合
if (out.includes('<admin-page-data-panel') && out.includes('</el-card>')) {
out = out.replace(/<\/el-table>\s*\n\s*<\/el-card>/, '</el-table>\n </admin-page-data-panel>')
}
// table 上的 v-loading 移到 panel(若 panel 还没有)
out = out.replace(
/<admin-page-data-panel>\s*\n(\s*)<el-table([^>]*?)v-loading="([^"]+)"([^>]*)>/,
'<admin-page-data-panel v-loading="$3">\n$1<el-table$2$4>'
)
out = out.replace(/<el-table([^>]*?)v-loading="([^"]+)"([^>]*)>/, '<el-table$1$3>')
if (out === content || !out.includes('admin-page-data-panel')) return null
return out
}
const targets = extraPaths.length
? extraPaths.map((p) => path.resolve(p))
: defaultTargets.map((p) => path.join(viewsRoot, p))
let migrated = 0
let skipped = 0
for (const file of targets) {
if (!fs.existsSync(file)) {
console.warn('skip (missing):', path.relative(viewsRoot, file))
skipped++
continue
}
const original = fs.readFileSync(file, 'utf8')
const result = migrate(original)
if (!result) {
console.log('skip (no match):', path.relative(viewsRoot, file))
skipped++
continue
}
if (dryRun) {
console.log('would migrate:', path.relative(viewsRoot, file))
} else {
fs.writeFileSync(file, result, 'utf8')
console.log('migrated:', path.relative(viewsRoot, file))
}
migrated++
}
console.log(`\nDone. migrated=${migrated} skipped=${skipped}${dryRun ? ' (dry-run)' : ''}`)
@@ -0,0 +1,24 @@
<template>
<section class="admin-data-panel" v-loading="loading">
<div v-if="$slots.toolbar" class="admin-data-panel__toolbar">
<slot name="toolbar" />
</div>
<div class="admin-data-panel__body">
<slot />
</div>
<div v-if="$slots.footer" class="admin-data-panel__footer">
<slot name="footer" />
</div>
</section>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
loading?: boolean
}>(),
{
loading: false
}
)
</script>
@@ -0,0 +1,60 @@
<template>
<section class="admin-filter-panel" :class="{ 'is-collapsed': collapsed }">
<div v-if="collapsible" class="admin-filter-panel__head">
<span class="admin-filter-panel__label">{{ title }}</span>
<el-button class="admin-filter-panel__toggle" link type="primary" @click="collapsed = !collapsed">
{{ collapsed ? '展开筛选' : '收起筛选' }}
</el-button>
</div>
<div class="admin-filter-panel__body">
<slot />
</div>
</section>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
/** 是否显示收起/展开 */
collapsible?: boolean
/** 筛选区标题 */
title?: string
}>(),
{
collapsible: true,
title: '筛选条件'
}
)
const collapsed = ref(false)
</script>
@@ -0,0 +1,24 @@
<template>
<section class="admin-form-panel">
<header v-if="title || $slots.header" class="admin-form-panel__header">
<slot name="header">
<h2 v-if="title" class="admin-form-panel__title">{{ title }}</h2>
</slot>
</header>
<div class="admin-form-panel__body">
<slot />
</div>
<footer v-if="$slots.footer" class="admin-form-panel__footer">
<slot name="footer" />
</footer>
</section>
</template>
<script setup lang="ts">
defineProps({
title: {
type: String,
default: ''
}
})
</script>
@@ -0,0 +1,5 @@
<template>
<div class="admin-page-actions">
<slot />
</div>
</template>
@@ -0,0 +1,16 @@
<template>
<div class="admin-stat-grid" :class="[`admin-stat-grid--cols-${columns}`]">
<slot />
</div>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
columns?: 2 | 3 | 4 | 5
}>(),
{
columns: 4
}
)
</script>
@@ -0,0 +1,27 @@
<template>
<article class="admin-stat-item" :class="[`admin-stat-item--${tone}`]">
<div class="admin-stat-item__label">{{ label }}</div>
<div class="admin-stat-item__value" :class="{ 'is-money': money }">
<slot>{{ value }}</slot>
</div>
<p v-if="hint" class="admin-stat-item__hint">{{ hint }}</p>
</article>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
label: string
value?: string | number
hint?: string
money?: boolean
tone?: 'default' | 'primary' | 'success' | 'warning'
}>(),
{
value: '',
hint: '',
money: false,
tone: 'default'
}
)
</script>
+7 -3
View File
@@ -17,14 +17,18 @@ defineProps({
<style scoped lang="scss">
.footer-btns {
height: 60px;
height: 64px;
&__content {
bottom: 0;
height: 60px;
height: 64px;
right: 0;
left: 0;
z-index: 99;
@apply flex justify-center items-center shadow bg-body;
padding: 0 20px;
border-top: 1px solid var(--el-border-color-lighter);
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.04);
@apply flex justify-center items-center gap-3 bg-body;
}
}
</style>
+228
View File
@@ -0,0 +1,228 @@
<template>
<div class="page-stage admin-page" :class="{ 'page-stage--flat': hideChrome }">
<header v-if="showHeader" class="page-stage__hero">
<div class="page-stage__hero-glow" aria-hidden="true"></div>
<div class="page-stage__hero-inner">
<div class="page-stage__intro">
<h1 class="page-stage__title">{{ pageTitle }}</h1>
<p v-if="pageDesc" class="page-stage__desc">{{ pageDesc }}</p>
</div>
<div v-if="$slots.actions" class="page-stage__actions">
<slot name="actions" />
</div>
</div>
</header>
<div class="page-stage__body">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import { PageEnum } from '@/enums/pageEnum'
const route = useRoute()
const hideChrome = computed(() => {
if (route.meta?.hidePageHeader) return true
if (route.path === PageEnum.INDEX || route.path === `${PageEnum.INDEX}/`) return true
const title = (route.meta?.title as string) || ''
return title.includes('工作台')
})
const pageTitle = computed(() => {
const matched = route.matched.filter((item) => item.meta?.title)
const last = matched[matched.length - 1]
return (last?.meta?.title as string) || ''
})
const pageDesc = computed(() => (route.meta?.pageDesc as string) || '')
const showHeader = computed(() => !hideChrome.value && Boolean(pageTitle.value))
</script>
<style lang="scss" scoped>
.page-stage {
min-height: 100%;
}
.page-stage__hero {
position: relative;
margin-bottom: 20px;
padding: 22px 24px;
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
background: var(--admin-surface-glass);
backdrop-filter: blur(16px) saturate(140%);
box-shadow: var(--el-box-shadow-light);
overflow: hidden;
}
.page-stage__hero-glow {
position: absolute;
inset: 0;
background:
linear-gradient(120deg, rgba(6, 182, 212, 0.12) 0%, transparent 42%),
linear-gradient(300deg, rgba(16, 185, 129, 0.1) 0%, transparent 38%);
pointer-events: none;
}
.page-stage__hero-inner {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 12px 20px;
}
.page-stage__title {
margin: 0;
font-size: 24px;
font-weight: 800;
line-height: 1.2;
letter-spacing: -0.03em;
color: var(--el-text-color-primary);
background: var(--admin-brand-gradient);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.page-stage__desc {
margin: 8px 0 0;
font-size: 14px;
line-height: 1.5;
color: var(--el-text-color-secondary);
max-width: 62ch;
}
.page-stage__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.page-stage__body {
min-width: 0;
}
.page-stage--flat .page-stage__hero {
display: none;
}
</style>
+20 -20
View File
@@ -1,28 +1,28 @@
const defaultSetting = {
showCrumb: true, // 是否显示面包屑
showLogo: false, // 是否显示logo
isUniqueOpened: true, //只展开一个一级菜单
sideWidth: 183, //侧边栏宽度
sideTheme: 'dark', //侧边栏主题
sideDarkColor: '#1d2124', //侧边栏深色主题颜色
openMultipleTabs: true, // 是否开启多标签tab栏
theme: '#4A5DFF', //主题色
successTheme: '#67c23a', //成功主题色
warningTheme: '#e6a23c', //警告主题色
dangerTheme: '#f56c6c', //危险主题色
errorTheme: '#f56c6c', //错误主题色
infoTheme: '#909399' //信息主题色
showCrumb: false,
showLogo: true,
isUniqueOpened: true,
sideWidth: 248,
sideTheme: 'dark',
sideDarkColor: '#060d18',
openMultipleTabs: true,
theme: '#06b6d4',
successTheme: '#10b981',
warningTheme: '#f59e0b',
dangerTheme: '#ef4444',
errorTheme: '#ef4444',
infoTheme: '#6366f1'
}
/** 本地 setting 缓存结构版本。提升后仅对低于该版本的老缓存执行 SETTING_SCHEMA_MIGRATIONS */
export const SETTING_SCHEMA_VERSION = 1
export const SETTING_SCHEMA_VERSION = 6
/**
* 按版本写入 defaultSetting 中的键(老用户 localStorage 会长期盖住 config 默认值)。
* 以后若要再推一批新默认值:把 SETTING_SCHEMA_VERSION +1,并为本版本追加一条迁移键列表。
*/
export const SETTING_SCHEMA_MIGRATIONS: Record<number, (keyof typeof defaultSetting)[]> = {
1: ['sideTheme', 'sideDarkColor']
1: ['sideTheme', 'sideDarkColor'],
2: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideDarkColor', 'sideWidth', 'showLogo'],
3: ['theme', 'sideDarkColor', 'sideWidth', 'showCrumb', 'showLogo'],
4: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideDarkColor'],
5: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideDarkColor'],
6: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideDarkColor', 'sideWidth']
}
export default defaultSetting
@@ -1,5 +1,5 @@
<template>
<el-breadcrumb class="app-breadcrumb">
<el-breadcrumb class="app-breadcrumb" separator="/">
<el-breadcrumb-item v-for="item in breadcrumbs" :key="item.path">
{{ item.meta.title }}
</el-breadcrumb-item>
@@ -21,22 +21,24 @@ useWatchRoute((route) => {
})
</script>
<style scoped lang="scss">
.app-breadcrumb {
:deep(.el-breadcrumb__item) {
.el-breadcrumb__inner {
color: #303133;
color: var(--el-text-color-secondary);
font-weight: 500;
font-size: var(--el-font-size-small);
}
&:last-child .el-breadcrumb__inner {
color: #303133;
color: var(--el-text-color-primary);
font-weight: 600;
}
}
:deep(.el-breadcrumb__separator) {
color: #606266;
color: var(--el-text-color-placeholder);
font-weight: 400;
}
}
</style>
@@ -1,7 +1,7 @@
<template>
<header class="header">
<div class="navbar">
<div class="flex-1 flex">
<div class="flex-1 flex items-center gap-1 min-w-0">
<div class="navbar-item">
<el-tooltip
class="box-item"
@@ -17,11 +17,14 @@
<refresh />
</el-tooltip>
</div>
<div class="flex items-center px-2" v-if="!isMobile && settingStore.showCrumb">
<div
class="hidden md:flex items-center min-w-0 px-2"
v-if="settingStore.showCrumb && breadcrumbs.length"
>
<breadcrumb />
</div>
</div>
<div class="flex">
<div class="flex items-center gap-1">
<div class="navbar-item" v-if="!isMobile">
<el-tooltip
class="box-item"
@@ -36,12 +39,7 @@
<user-drop-down />
</div>
<div class="navbar-item">
<el-tooltip
class="box-item"
effect="dark"
content="主题设置"
placement="bottom"
>
<el-tooltip class="box-item" effect="dark" content="主题设置" placement="bottom">
<setting />
</el-tooltip>
</div>
@@ -52,8 +50,10 @@
</template>
<script setup lang="ts">
import type { RouteLocationMatched } from 'vue-router'
import { useFullscreen } from '@vueuse/core'
import { useWatchRoute } from '@/hooks/useWatchRoute'
import useAppStore from '@/stores/modules/app'
import useSettingStore from '@/stores/modules/setting'
@@ -70,14 +70,20 @@ const isMobile = computed(() => appStore.isMobile)
const isCollapsed = computed(() => appStore.isCollapsed)
const settingStore = useSettingStore()
const { isFullscreen } = useFullscreen()
const breadcrumbs = ref<RouteLocationMatched[]>([])
useWatchRoute((route) => {
breadcrumbs.value = route.matched.filter((item) => item.meta && item.meta.title)
})
</script>
<style lang="scss">
.navbar {
height: var(--navbar-height);
@apply flex px-2 bg-body;
@apply flex px-3 bg-body;
.navbar-item {
@apply h-full flex justify-center items-center hover:bg-page;
@apply h-full flex justify-center items-center;
}
}
</style>
@@ -1,6 +1,6 @@
<template>
<div class="app-tabs pl-4 flex bg-body">
<div class="flex-1 min-w-0">
<div class="app-tabs flex bg-body">
<div class="flex-1 min-w-0 pl-3">
<el-tabs
:model-value="currentTab"
:closable="tabsLists.length > 1"
@@ -13,7 +13,7 @@
</el-tabs>
</div>
<el-dropdown @command="handleCommand">
<span class="flex items-center px-3">
<span class="tabs-more-btn flex items-center px-3">
<icon :size="16" name="el-icon-arrow-down" />
</span>
<template #dropdown>
@@ -60,61 +60,84 @@ const handleCommand = (command: any) => {
</script>
<style lang="scss" scoped>
.app-tabs {
@apply border-t border-br;
height: var(--tabs-height);
border-top: 1px solid var(--admin-header-border);
background: var(--admin-surface-glass);
backdrop-filter: blur(12px);
.tabs-more-btn {
height: var(--tabs-height);
color: var(--el-text-color-secondary);
cursor: pointer;
transition: color 0.2s ease, background-color 0.2s ease;
&:hover {
color: var(--admin-brand-primary-dark);
background: var(--admin-brand-gradient-soft);
}
}
:deep(.el-tabs) {
height: 40px;
height: var(--tabs-height);
.el-tabs {
&__header {
margin-bottom: 0;
}
&__content {
display: none;
}
&__nav-next,
&__nav-prev {
@apply text-xl;
@apply text-lg;
}
&__nav-wrap::after {
height: 0;
}
&__item {
font-weight: normal;
padding: 0 15px !important;
font-weight: 600;
font-size: var(--el-font-size-small);
padding: 0 16px !important;
height: calc(var(--tabs-height) - 8px);
margin-top: 4px;
border-radius: var(--admin-radius-md) var(--admin-radius-md) 0 0;
box-sizing: border-box;
color: var(--el-text-color-secondary);
transition: color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
&:hover {
color: var(--admin-brand-primary-dark);
}
&.is-active {
color: var(--el-text-color-primary);
background-color: var(--el-color-primary-light-9);
&::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
background-color: var(--el-color-primary);
margin-right: 6px;
border-radius: 50%;
vertical-align: 2px;
}
color: var(--admin-brand-primary-dark);
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-bottom-color: transparent;
box-shadow: var(--el-box-shadow-lighter);
&::before,
&::after {
position: absolute;
content: '';
display: block;
top: 0;
height: 2px;
left: 0;
width: 100%;
background-color: var(--el-color-primary);
display: none;
}
}
.is-icon-close {
color: var(--el-text-color-regular);
color: var(--el-text-color-placeholder);
vertical-align: -2px;
border-radius: var(--admin-radius-sm);
&:hover {
color: var(--color-white);
background-color: var(--el-color-danger);
}
}
}
&__active-bar {
display: none;
}
@@ -1,9 +1,11 @@
<template>
<el-dropdown class="px-2" @command="handleCommand">
<div class="flex items-center">
<el-avatar :size="34" :src="userInfo.avatar" />
<div class="ml-3 mr-1">{{ userInfo.name }}</div>
<icon name="el-icon-ArrowDown" />
<el-dropdown class="user-dropdown px-1" @command="handleCommand">
<div class="user-trigger flex items-center gap-2.5 px-2 py-1 rounded-md cursor-pointer">
<el-avatar :size="32" :src="userInfo.avatar" />
<span class="user-name max-w-[120px] truncate text-sm font-medium text-tx-primary">{{
userInfo.name
}}</span>
<icon name="el-icon-ArrowDown" :size="14" />
</div>
<template #dropdown>
@@ -41,3 +43,13 @@ const handleCommand = async (command: string) => {
}
}
</script>
<style scoped lang="scss">
.user-trigger {
transition: background-color 0.2s ease;
&:hover {
background: var(--el-fill-color-light);
}
}
</style>
+37 -8
View File
@@ -1,27 +1,56 @@
<template>
<main class="main-wrap h-full bg-page">
<main class="main-wrap h-full">
<el-scrollbar>
<div class="px-2 py-4">
<router-view v-if="isRouteShow" v-slot="{ Component, route }">
<keep-alive :include="includeList" :max="20">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
<div class="main-stage">
<page-shell v-if="isRouteShow">
<router-view v-slot="{ Component, route }">
<keep-alive :include="includeList" :max="20">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
</page-shell>
</div>
</el-scrollbar>
</main>
</template>
<script setup lang="ts">
import PageShell from '@/components/page-shell/index.vue'
import useAppStore from '@/stores/modules/app'
import useTabsStore from '@/stores/modules/multipleTabs'
import useSettingStore from '@/stores/modules/setting'
const appStore = useAppStore()
const tabsStore = useTabsStore()
const settingStore = useSettingStore()
const isRouteShow = computed(() => appStore.isRouteShow)
const includeList = computed(() => (settingStore.openMultipleTabs ? tabsStore.getCacheTabList : []))
</script>
<style></style>
@@ -100,7 +100,7 @@ import theme_light from '@/assets/images/theme_white.png'
import useSettingStore from '@/stores/modules/setting'
const settingStore = useSettingStore()
const predefineColors = ref(['#409EFF', '#28C76F', '#EA5455', '#FF9F43', '#01CFE8', '#4A5DFF'])
const predefineColors = ref(['#06b6d4', '#10b981', '#0891b2', '#6366f1', '#f59e0b', '#ef4444', '#64748b'])
const sideThemeList = [
{
type: 'dark',
@@ -68,32 +68,42 @@ const themeClass = computed(() => `theme-${props.theme}`)
.el-menu {
:deep(.el-menu-item) {
&.is-active {
@apply bg-primary border-primary;
@apply bg-primary;
box-shadow: inset 3px 0 0 var(--el-color-primary-light-3);
}
}
}
:deep(.el-menu--collapse) {
.el-sub-menu.is-active .el-sub-menu__title {
@apply bg-primary #{!important};
}
}
}
&.theme-light {
:deep(.el-menu) {
.el-menu-item {
border-color: transparent;
&.is-active {
@apply bg-primary-light-9 border-r-2 border-primary;
@apply bg-primary-light-9;
color: var(--el-color-primary);
font-weight: 600;
box-shadow: inset 3px 0 0 var(--el-color-primary);
}
}
.el-menu-item:hover,
.el-sub-menu__title:hover {
color: var(--el-color-primary);
}
}
}
.el-menu {
border-right: none;
&:not(.el-menu--collapse) {
width: var(--aside-width);
}
@@ -1,5 +1,15 @@
<template>
<div class="side" :style="sideStyle">
<div v-if="showBrandStrip" class="sidebar-brand">
<div class="sidebar-brand__mark">ZY</div>
<overflow-tooltip
class="sidebar-brand__name"
:content="config.web_name"
:teleported="true"
placement="bottom"
overflo-type="unset"
/>
</div>
<side-logo v-if="settingStore.showLogo" :show-title="!isCollapsed" :theme="sideTheme" />
<side-menu
:routes="routes"
@@ -25,17 +35,19 @@ const appStore = useAppStore()
const isCollapsed = computed(() => {
if (appStore.isMobile) {
return false
} else {
return appStore.isCollapsed
}
return appStore.isCollapsed
})
const settingStore = useSettingStore()
const sideTheme = computed(() => settingStore.sideTheme)
const userStore = useUserStore()
const config = computed(() => appStore.config)
const routes = computed(() => userStore.routes)
const showBrandStrip = computed(() => !settingStore.showLogo && !isCollapsed.value)
const sideStyle = computed(() => {
return sideTheme.value == 'dark'
? {
@@ -43,6 +55,7 @@ const sideStyle = computed(() => {
}
: ''
})
const menuProp = computed(() => {
return {
backgroundColor: sideTheme.value == 'dark' ? settingStore.sideDarkColor : '',
@@ -50,6 +63,7 @@ const menuProp = computed(() => {
activeTextColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : ''
}
})
const handleSelect = () => {
if (appStore.isMobile) {
appStore.toggleCollapsed(true)
@@ -61,7 +75,8 @@ const handleSelect = () => {
.side {
position: relative;
z-index: 999;
@apply border-r border-br-light h-full flex flex-col;
background-color: var(--side-dark-color, var(--el-bg-color));
@apply h-full flex flex-col;
border-right: 1px solid var(--admin-sidebar-border);
background-color: var(--side-dark-color, var(--sidebar-dark-bg));
}
</style>
+298
View File
@@ -0,0 +1,298 @@
/**
* Admin 页面架构 v2 - 大改版面板系统
*/
.admin-page .page-stage__body {
display: flex;
flex-direction: column;
gap: 16px;
}
/* ---------- FilterPanel ---------- */
.admin-filter-panel {
position: relative;
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
background: var(--admin-surface-glass);
backdrop-filter: blur(12px);
box-shadow: var(--el-box-shadow-lighter);
overflow: hidden;
&::before {
content: '';
position: absolute;
inset: 0 0 auto;
height: 3px;
background: var(--admin-brand-gradient);
}
}
.admin-filter-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 22px 0;
}
.admin-filter-panel__label {
font-size: 14px;
font-weight: 700;
color: var(--el-text-color-primary);
}
.admin-filter-panel__toggle {
padding: 0;
font-size: 13px;
font-weight: 600;
}
.admin-filter-panel__body {
padding: 14px 22px 12px;
}
.admin-filter-panel.is-collapsed .admin-filter-panel__body {
display: none;
}
.admin-filter-panel .el-form--inline {
margin-bottom: 0;
.el-form-item {
margin-bottom: 12px;
margin-right: 18px;
}
.el-form-item__label {
font-weight: 600;
color: var(--el-text-color-regular);
}
}
/* ---------- DataPanel ---------- */
.admin-data-panel {
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
background: var(--admin-surface-elevated);
box-shadow: var(--el-box-shadow);
overflow: hidden;
}
.admin-data-panel__toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 16px 22px;
border-bottom: 1px solid var(--el-border-color-extra-light);
background: var(--admin-brand-gradient-soft);
}
.admin-data-panel__body {
padding: 0 22px 18px;
}
.admin-data-panel__toolbar + .admin-data-panel__body {
padding-top: 16px;
}
.admin-data-panel__footer {
display: flex;
justify-content: flex-end;
padding: 0 22px 18px;
}
/* ---------- FormPanel ---------- */
.admin-form-panel {
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
background: var(--admin-surface-elevated);
box-shadow: var(--el-box-shadow-light);
overflow: hidden;
}
.admin-form-panel__header {
padding: 16px 22px;
border-bottom: 1px solid var(--el-border-color-extra-light);
background: var(--admin-brand-gradient-soft);
}
.admin-form-panel__title {
margin: 0;
font-size: 17px;
font-weight: 700;
color: var(--el-text-color-primary);
}
.admin-form-panel__body {
padding: 22px;
}
.admin-form-panel__footer {
display: flex;
justify-content: center;
gap: 12px;
padding: 16px 22px 22px;
border-top: 1px solid var(--el-border-color-extra-light);
background: var(--el-fill-color-lighter);
}
/* ---------- StatGrid ---------- */
.admin-stat-grid {
display: grid;
gap: 14px;
}
.admin-stat-grid--cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.admin-stat-grid--cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.admin-stat-grid--cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.admin-stat-grid--cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.admin-stat-item {
position: relative;
padding: 18px 20px;
border-radius: var(--admin-radius-lg);
border: 1px solid var(--el-border-color-lighter);
background: var(--admin-surface-elevated);
box-shadow: var(--el-box-shadow-lighter);
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&::before {
content: '';
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: var(--el-border-color);
}
&:hover {
transform: translateY(-2px);
box-shadow: var(--el-box-shadow-light);
}
}
.admin-stat-item--primary {
&::before { background: var(--admin-brand-gradient); }
border-color: rgba(6, 182, 212, 0.2);
background: linear-gradient(145deg, rgba(6, 182, 212, 0.08), rgba(16, 185, 129, 0.05));
}
.admin-stat-item--success {
&::before { background: #10b981; }
border-color: rgba(16, 185, 129, 0.22);
}
.admin-stat-item--warning {
&::before { background: #f59e0b; }
border-color: rgba(245, 158, 11, 0.22);
}
.admin-stat-item__label {
font-size: 13px;
font-weight: 500;
color: var(--el-text-color-secondary);
}
.admin-stat-item__value {
margin-top: 10px;
font-size: 30px;
font-weight: 800;
line-height: 1.05;
letter-spacing: -0.03em;
color: var(--el-text-color-primary);
font-variant-numeric: tabular-nums;
}
.admin-stat-item__value.is-money {
font-size: 26px;
}
.admin-stat-item__hint {
margin: 8px 0 0;
font-size: 12px;
color: var(--el-text-color-placeholder);
}
/* ---------- Tables ---------- */
.admin-data-panel .el-table,
.admin-page .el-card .el-table {
--el-table-border-color: transparent;
border-radius: var(--admin-radius-md);
thead th.el-table__cell {
position: sticky;
top: 0;
z-index: 2;
background: var(--table-header-bg-color) !important;
border-bottom: 1px solid var(--el-border-color-lighter);
font-weight: 700;
font-size: var(--el-font-size-small);
}
td.el-table__cell {
border-bottom: 1px solid var(--el-border-color-extra-light);
}
.el-table__row:hover > td.el-table__cell {
background-color: rgba(6, 182, 212, 0.04) !important;
}
.el-table__row.current-row > td.el-table__cell {
background-color: rgba(6, 182, 212, 0.08) !important;
}
}
/* ---------- Legacy cards ---------- */
.admin-page .page-stage__body {
> div > .el-card:first-child:has(.el-form),
> .el-card:first-child:has(.el-form) {
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow-lighter);
}
> div > .el-card + .el-card,
> .el-card + .el-card {
border-radius: var(--admin-radius-xl);
box-shadow: var(--el-box-shadow);
}
}
.workbench-page {
margin: 0;
}
@media (max-width: 1200px) {
.admin-stat-grid--cols-4,
.admin-stat-grid--cols-5 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.admin-filter-panel__body,
.admin-data-panel__body,
.admin-data-panel__toolbar,
.admin-data-panel__footer {
padding-left: 16px;
padding-right: 16px;
}
.admin-stat-grid--cols-2,
.admin-stat-grid--cols-3,
.admin-stat-grid--cols-4,
.admin-stat-grid--cols-5 {
grid-template-columns: 1fr;
}
.admin-stat-item:hover {
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
.admin-stat-item {
transition: none;
}
}
+190
View File
@@ -0,0 +1,190 @@
/* Admin Shell v2 - 互联网医院工作台大改版 */
.layout-default {
background: var(--sidebar-dark-bg);
}
/* ---------- Sidebar ---------- */
.app-aside .side {
position: relative;
&::before {
content: '';
position: absolute;
inset: 0 auto 0 0;
width: var(--sidebar-rail-width);
background: var(--admin-brand-gradient);
z-index: 2;
pointer-events: none;
}
&::after {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(circle at 0% 0%, var(--admin-brand-mesh-a), transparent 45%),
radial-gradient(circle at 100% 100%, var(--admin-brand-mesh-b), transparent 40%);
pointer-events: none;
}
}
.sidebar-brand {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 12px;
min-height: calc(var(--navbar-height) + 4px);
padding: 0 18px 0 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.sidebar-brand__mark {
width: 38px;
height: 38px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.1em;
color: #ffffff;
background: var(--admin-brand-gradient);
box-shadow: var(--admin-brand-glow);
flex-shrink: 0;
}
.sidebar-brand__name {
font-size: 15px;
font-weight: 700;
color: rgba(255, 255, 255, 0.95);
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.menu.theme-dark .el-menu {
--el-menu-bg-color: transparent;
--el-menu-hover-bg-color: var(--sidebar-dark-hover);
--el-menu-active-color: #ffffff;
--el-menu-text-color: rgba(255, 255, 255, 0.62);
padding: 12px 10px 16px;
position: relative;
z-index: 1;
}
.menu.theme-dark .el-menu .el-menu-item,
.menu.theme-dark .el-menu .el-sub-menu__title {
margin: 3px 8px;
border-radius: var(--admin-radius-md);
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, transform 0.12s ease;
}
.menu.theme-dark .el-menu .el-menu-item.is-active {
background: var(--sidebar-dark-active) !important;
color: #ffffff;
font-weight: 600;
box-shadow:
inset 0 0 0 1px rgba(6, 182, 212, 0.28),
0 8px 24px rgba(6, 182, 212, 0.15);
}
.menu.theme-dark .el-menu .el-menu-item:hover,
.menu.theme-dark .el-menu .el-sub-menu__title:hover {
background: var(--sidebar-dark-hover);
color: #ffffff;
}
.menu.theme-light .el-menu .el-menu-item.is-active {
font-weight: 600;
color: var(--admin-brand-primary-dark);
background: var(--admin-brand-gradient-soft);
box-shadow: inset 3px 0 0 var(--admin-brand-primary);
}
/* ---------- Header ---------- */
.app-header {
position: relative;
z-index: 20;
background: var(--admin-surface-glass);
backdrop-filter: blur(20px) saturate(160%);
border-bottom: 1px solid var(--admin-header-border);
&::after {
content: '';
position: absolute;
inset: auto 0 0;
height: 2px;
background: var(--admin-brand-gradient);
opacity: 0.75;
pointer-events: none;
}
}
:root.dark .app-header {
background: rgba(15, 23, 42, 0.88);
}
.navbar-tool {
width: 38px;
height: 38px;
border-radius: var(--admin-radius-md);
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--el-text-color-regular);
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.12s ease, box-shadow 0.2s ease;
&:hover {
background: var(--admin-brand-gradient-soft);
color: var(--admin-brand-primary-dark);
box-shadow: var(--el-box-shadow-lighter);
}
&:active {
transform: scale(0.96);
}
}
/* ---------- Main stage (去掉双层白盒) ---------- */
.main-wrap {
position: relative;
background:
radial-gradient(circle at 0% 0%, var(--admin-brand-mesh-a), transparent 34%),
radial-gradient(circle at 100% 0%, var(--admin-brand-mesh-c), transparent 30%),
radial-gradient(circle at 50% 100%, var(--admin-brand-mesh-b), transparent 38%),
var(--el-bg-color-page);
}
.main-stage {
max-width: var(--content-max-width);
margin: 0 auto;
padding: var(--stage-padding-y) var(--stage-padding-x);
min-height: calc(100vh - var(--navbar-height) - var(--tabs-height, 0px));
}
@media (max-width: 768px) {
:root {
--stage-padding-x: 14px;
--stage-padding-y: 14px;
}
}
@media (prefers-reduced-motion: reduce) {
.navbar-tool,
.menu.theme-dark .el-menu .el-menu-item,
.menu.theme-dark .el-menu .el-sub-menu__title {
transition: none;
}
}
@media (prefers-reduced-transparency: reduce) {
.app-header {
backdrop-filter: none;
background: var(--el-bg-color);
}
}
+46 -31
View File
@@ -1,43 +1,58 @@
:root.dark {
color-scheme: dark;
--table-header-bg-color: var(--el-bg-color);
--el-bg-color-page: #0a0a0a;
--el-bg-color: #1d2124;
--el-bg-color-overlay: #1d1e1f;
--el-text-color-primary: #e5eaf3;
--el-text-color-regular: #cfd3dc;
--el-text-color-secondary: #a3a6ad;
--el-text-color-placeholder: #8d9095;
--el-text-color-disabled: #6c6e72;
--el-border-color-darker: #636466;
--el-border-color-dark: #58585b;
--el-border-color: #4c4d4f;
--el-border-color-light: #414243;
--el-border-color-lighter: #363637;
--el-border-color-extra-light: #2b2b2c;
--el-fill-color-darker: #424243;
--el-fill-color-dark: #39393a;
--el-fill-color: #303030;
--el-fill-color-light: #262727;
--el-fill-color-lighter: #1d1d1d;
--el-fill-color-extra-light: #191919;
--table-header-bg-color: rgba(6, 182, 212, 0.12);
--sidebar-dark-bg: #030712;
--sidebar-dark-hover: rgba(255, 255, 255, 0.05);
--sidebar-dark-active: rgba(6, 182, 212, 0.2);
--admin-surface-elevated: #111827;
--admin-surface-muted: rgba(17, 24, 39, 0.88);
--admin-surface-glass: rgba(17, 24, 39, 0.86);
--el-bg-color-page: #030712;
--el-bg-color: #0f172a;
--el-bg-color-overlay: #1e293b;
--el-text-color-primary: #f1f5f9;
--el-text-color-regular: #cbd5e1;
--el-text-color-secondary: #94a3b8;
--el-text-color-placeholder: #64748b;
--el-text-color-disabled: #475569;
--el-border-color-darker: #64748b;
--el-border-color-dark: #475569;
--el-border-color: rgba(6, 182, 212, 0.18);
--el-border-color-light: rgba(6, 182, 212, 0.12);
--el-border-color-lighter: rgba(255, 255, 255, 0.06);
--el-border-color-extra-light: rgba(255, 255, 255, 0.04);
--el-fill-color-darker: #334155;
--el-fill-color-dark: #1e293b;
--el-fill-color: #1e293b;
--el-fill-color-light: #172033;
--el-fill-color-lighter: #131c2e;
--el-fill-color-extra-light: #0f172a;
--el-fill-color-blank: var(--el-bg-color);
--el-mask-color: rgba(0, 0, 0, 0.8);
--el-mask-color-extra-light: rgba(0, 0, 0, 0.3);
--el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.36), 0px 8px 20px rgba(0, 0, 0, 0.72);
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.72);
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.72);
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.72), 0px 12px 32px #000000,
0px 8px 16px -8px #000000 !important;
/* wangeditor主题 */
--el-mask-color: rgba(2, 6, 23, 0.78);
--el-mask-color-extra-light: rgba(2, 6, 23, 0.42);
--el-box-shadow: 0 4px 24px rgba(0, 0, 0, 0.28);
--el-box-shadow-light: 0 2px 16px rgba(0, 0, 0, 0.22);
--el-box-shadow-lighter: 0 1px 4px rgba(0, 0, 0, 0.16);
--el-box-shadow-dark: 0 16px 48px rgba(0, 0, 0, 0.36);
--admin-header-border: rgba(6, 182, 212, 0.14);
--admin-sidebar-border: rgba(255, 255, 255, 0.04);
--admin-brand-mesh-a: rgba(6, 182, 212, 0.12);
--admin-brand-mesh-b: rgba(16, 185, 129, 0.08);
--admin-brand-mesh-c: rgba(99, 102, 241, 0.06);
--w-e-textarea-bg-color: var(--el-bg-color);
--w-e-textarea-color: var(--el-text-color-primary);
--w-e-textarea-border-color: var(--el-border-color);
--w-e-textarea-slight-border-color: var(--el-border-color-light);
--w-e-textarea-slight-color: var(--el-border-color);
--w-e-textarea-slight-bg-color: var(--el-bg-color-page);
/* --w-e-textarea-selected-border-color: #b4d5ff;
--w-e-textarea-handler-bg-color: #4290f7; */
--w-e-toolbar-color: var(--el-text-color-primary);
--w-e-toolbar-bg-color: var(--el-bg-color);
--w-e-toolbar-active-color: var(--el-text-color-primary);
+246 -26
View File
@@ -1,14 +1,20 @@
:root {
// 确保消息提示在最上层
/* Messages & notifications */
.el-message {
z-index: 9999 !important;
border-radius: var(--admin-radius-md);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow);
}
.el-notification {
z-index: 9999 !important;
border-radius: var(--admin-radius-lg);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow);
}
// 弹窗居中
/* Dialog */
.el-overlay-dialog {
display: flex;
justify-content: center;
@@ -19,11 +25,14 @@
.el-dialog {
--el-dialog-content-font-size: var(--el-font-size-base);
--el-dialog-margin-top: 50px;
max-width: calc(100vw - 30px);
--el-dialog-border-radius: var(--admin-radius-lg);
max-width: calc(100vw - 32px);
flex: none;
display: flex;
flex-direction: column;
border-radius: 5px;
border-radius: var(--admin-radius-lg);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow-dark);
&.body-padding .el-dialog__body {
padding: 0;
@@ -31,50 +40,158 @@
.el-dialog__body {
flex: 1;
padding: 15px 20px;
padding: 16px 20px 20px;
}
.el-dialog__header {
font-size: var(--el-font-size-large);
font-weight: 600;
padding: 16px 20px 12px;
margin-right: 0;
border-bottom: 1px solid var(--el-border-color-extra-light);
}
.el-dialog__footer {
padding: 12px 20px 16px;
border-top: 1px solid var(--el-border-color-extra-light);
}
}
}
/* Cards - applies to all list pages */
.el-card {
--el-card-border-radius: var(--admin-radius-lg);
border: 1px solid var(--el-border-color-lighter);
box-shadow: none;
background: var(--admin-surface-elevated);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
&.is-always-shadow,
&.is-hover-shadow:hover {
box-shadow: var(--el-box-shadow-light);
}
.el-card__header {
padding: 14px 18px;
border-bottom: 1px solid var(--el-border-color-extra-light);
font-weight: 600;
color: var(--el-text-color-primary);
}
.el-card__body {
padding: 18px;
}
}
/* Menu */
.el-menu {
user-select: none;
}
/* Drawer */
.el-drawer {
--el-drawer-padding-primary: 16px;
&__header {
margin-bottom: 0;
padding: 13px 16px;
padding: 14px 18px;
border-bottom: 1px solid var(--el-border-color-lighter);
font-weight: 600;
}
&__title {
@apply text-tx-primary;
}
}
.el-table {
--el-table-header-text-color: var(--el-text-color-primary);
--el-table-header-bg-color: var(--table-header-bg-color);
font-size: var(--el-font-size-base);
thead {
th {
font-weight: 400;
}
&__body {
padding: 16px 18px;
}
}
/* Table */
.el-table {
--el-table-header-text-color: var(--el-text-color-primary);
--el-table-header-bg-color: var(--table-header-bg-color);
--el-table-border-color: var(--el-border-color-extra-light);
--el-table-row-hover-bg-color: var(--el-fill-color-lighter);
font-size: var(--el-font-size-base);
border-radius: var(--admin-radius-md);
overflow: hidden;
thead {
th {
font-weight: 600;
font-size: var(--el-font-size-small);
letter-spacing: 0.01em;
}
th.el-table__cell {
background-color: var(--table-header-bg-color);
}
}
.el-table__cell {
padding: 12px 0;
}
}
/* Form controls */
.el-input-group__prepend {
background-color: var(--el-fill-color-blank);
background-color: var(--el-fill-color-light);
border-color: var(--el-border-color);
}
.el-checkbox {
--el-checkbox-font-size: var(--el-font-size-base);
}
.el-button {
--el-border-radius-base: var(--admin-radius-md);
font-weight: 500;
transition: transform 0.12s ease, box-shadow 0.2s ease, background-color 0.2s ease;
&:active:not(.is-disabled) {
transform: scale(0.98);
}
}
.el-button--primary:not(.is-link):not(.is-text):not(.is-plain) {
background: var(--admin-brand-gradient);
border: none;
box-shadow: var(--admin-brand-glow);
--el-button-bg-color: transparent;
--el-button-border-color: transparent;
--el-button-hover-bg-color: transparent;
--el-button-hover-border-color: transparent;
&:hover {
filter: brightness(1.05);
box-shadow: 0 12px 32px rgba(6, 182, 212, 0.32);
}
}
.el-button--primary.is-link,
.el-button--primary.is-text {
--el-button-hover-link-text-color: var(--admin-brand-primary-dark);
}
.el-button--primary.is-plain {
--el-button-bg-color: rgba(6, 182, 212, 0.1);
--el-button-border-color: rgba(6, 182, 212, 0.35);
--el-button-text-color: var(--admin-brand-primary-dark);
--el-button-hover-bg-color: rgba(6, 182, 212, 0.16);
--el-button-hover-border-color: var(--admin-brand-primary);
}
.el-button--large {
--el-border-radius-base: var(--admin-radius-md);
}
.el-button.is-round {
--el-border-radius-base: var(--admin-radius-round);
}
/* Popup menus */
.el-menu--popup-container {
&.theme-light {
.el-menu {
@@ -83,12 +200,14 @@
@apply bg-primary-light-9 border-primary border-r-2;
}
}
.el-menu-item:hover,
.el-sub-menu__title:hover {
color: var(--el-color-primary);
}
}
}
&.theme-dark {
.el-menu {
.el-menu-item {
@@ -101,52 +220,120 @@
}
.el-message-box {
--el-messagebox-width: 350px;
--el-messagebox-width: 380px;
--el-messagebox-border-radius: var(--admin-radius-lg);
}
.el-date-editor {
--el-date-editor-datetimerange-width: 380px;
.el-range-input {
font-size: var(--el-font-size-small);
}
}
.el-button--primary {
--el-button-hover-link-text-color: var(--el-color-primary-light-3);
}
.el-button--success {
--el-button-hover-link-text-color: var(--el-color-success-light-3);
}
.el-button--info {
--el-button-hover-link-text-color: var(--el-color-info-light-3);
}
.el-button--warning {
--el-button-hover-link-text-color: var(--el-color-warning-light-3);
}
.el-button--danger {
--el-button-hover-link-text-color: var(--el-color-danger-light-3);
}
.el-image__error {
font-size: 12px;
}
.el-tabs__nav-wrap::after {
height: 1px;
background: var(--el-border-color-extra-light);
}
.el-tabs__item {
font-weight: 500;
transition: color 0.2s ease;
&.is-active {
color: var(--el-color-primary);
font-weight: 600;
}
&:hover {
color: var(--el-color-primary-light-3);
}
}
.el-tabs__active-bar {
height: 3px;
border-radius: 999px;
background: var(--admin-brand-gradient);
}
.el-page-header {
&__breadcrumb {
margin-bottom: 0;
}
}
/* Tags */
.el-tag {
--el-tag-border-radius: var(--admin-radius-sm);
border: none;
font-weight: 500;
}
/* Pagination */
.el-pagination {
.el-pager li {
border-radius: var(--admin-radius-sm);
font-weight: 500;
}
.btn-prev,
.btn-next {
border-radius: var(--admin-radius-sm);
}
}
/* Alert */
.el-alert {
border-radius: var(--admin-radius-md);
border: 1px solid var(--el-border-color-extra-light);
}
/* Empty state */
.el-empty {
padding: 32px 0;
}
/* Descriptions */
.el-descriptions {
.el-descriptions__label {
font-weight: 500;
color: var(--el-text-color-secondary);
}
}
/* Focus rings */
.el-input,
.el-select,
.el-textarea {
@apply shadow-primary-light-8;
box-shadow: 0 0 0 0 var(--tw-shadow-color);
&:focus-within {
box-shadow: 0 0 0 2px var(--tw-shadow-color);
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
transition: box-shadow ease 0.1s;
transition: box-shadow ease 0.15s;
}
}
@@ -156,6 +343,7 @@
border-radius: var(--el-checkbox-border-radius);
box-shadow: 0 0 0 0 var(--tw-shadow-color);
&:active {
box-shadow: 0 0 0 2px var(--tw-shadow-color);
transition: box-shadow ease 0s;
@@ -173,29 +361,61 @@
.el-form-item.is-error .el-checkbox {
@apply shadow-danger-light-8;
}
/* Dropdown */
.el-dropdown-menu {
border-radius: var(--admin-radius-md);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow);
padding: 4px;
}
.el-dropdown-menu__item {
border-radius: var(--admin-radius-sm);
}
/* Popover / tooltip polish */
.el-popover.el-popper {
border-radius: var(--admin-radius-md);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow);
}
}
@media (max-width: 768px) {
.el-pagination > .el-pagination__jump {
display: none !important;
}
.el-pagination > .el-pagination__sizes {
display: none !important;
}
}
.el-button {
// 防止被tailwindcss默认样式覆盖
background-color: var(--el-button-bg-color, var(--el-color-white));
//覆盖el-button的点击样式
&:focus {
color: var(--el-button-text-color);
border-color: var(--el-button-border-color);
background-color: var(--el-button-bg-color);
}
&:hover {
color: var(--el-button-hover-text-color);
border-color: var(--el-button-hover-border-color);
background-color: var(--el-button-hover-bg-color);
}
}
/* Inline form filter blocks on list pages */
.el-card .el-form--inline {
.el-form-item {
margin-bottom: 12px;
}
}
/* Page section spacing between stacked cards */
.el-card + .el-card {
margin-top: 16px;
}
+4 -2
View File
@@ -1,5 +1,7 @@
@import 'element.scss';
@import 'dark.css';
@import 'var.css';
@import 'dark.css';
@import 'tailwind.css';
@import 'element.scss';
@import 'admin-shell.scss';
@import 'admin-pages.scss';
@import 'public.scss';
+48
View File
@@ -1,6 +1,10 @@
body {
@apply text-base text-tx-primary overflow-hidden min-w-[375px];
font-feature-settings: 'kern' 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.form-tips {
@apply text-tx-secondary text-xs leading-6 mt-1;
}
@@ -12,7 +16,51 @@ body {
visibility: hidden;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--el-border-color);
border-radius: 999px;
border: 2px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background: var(--el-border-color-dark);
background-clip: content-box;
}
/* NProgress */
#nprogress .bar {
@apply bg-primary #{!important};
height: 2px !important;
}
#nprogress .peg {
box-shadow: 0 0 8px var(--el-color-primary), 0 0 4px var(--el-color-primary) !important;
}
/* Shared page utilities */
.admin-page-title {
@apply text-xl font-semibold text-tx-primary tracking-tight;
}
.admin-page-desc {
@apply text-sm text-tx-secondary mt-1;
}
.admin-stat-value {
@apply text-3xl font-semibold text-tx-primary tabular-nums;
}
.admin-stat-label {
@apply text-sm text-tx-secondary;
}
+85 -35
View File
@@ -1,49 +1,99 @@
:root {
/* Typography */
--el-font-family: theme(fontFamily.sans);
--el-font-weight-primary: 400;
--el-menu-item-height: 46px;
--el-menu-sub-item-height: var(--el-menu-item-height);
--el-menu-icon-width: 18px;
--aside-width: 200px;
--navbar-height: 50px;
--color-white: #ffffff;
--table-header-bg-color: #f8f8f8;
--el-font-size-extra-large: 18px;
--el-menu-base-level-padding: 16px;
--el-menu-level-padding: 26px;
--el-font-size-large: 16px;
--el-font-size-medium: 15px;
--el-font-size-base: 14px;
--el-font-size-small: 13px;
--el-font-size-extra-small: 12px;
/* Brand - MedTech 绚丽临床 */
--admin-brand-primary: #06b6d4;
--admin-brand-primary-dark: #0891b2;
--admin-brand-accent: #10b981;
--admin-brand-secondary: #6366f1;
--admin-brand-success: #10b981;
--admin-brand-gradient: linear-gradient(120deg, #06b6d4 0%, #10b981 48%, #0891b2 100%);
--admin-brand-gradient-soft: linear-gradient(
120deg,
rgba(6, 182, 212, 0.16) 0%,
rgba(16, 185, 129, 0.1) 50%,
rgba(8, 145, 178, 0.08) 100%
);
--admin-brand-glow: 0 0 24px rgba(6, 182, 212, 0.35);
--admin-brand-mesh-a: rgba(6, 182, 212, 0.18);
--admin-brand-mesh-b: rgba(16, 185, 129, 0.14);
--admin-brand-mesh-c: rgba(99, 102, 241, 0.1);
/* Layout shell v2 */
--aside-width: 248px;
--navbar-height: 60px;
--tabs-height: 42px;
--stage-padding-x: 28px;
--stage-padding-y: 24px;
--content-max-width: 1680px;
--sidebar-rail-width: 3px;
/* Shape */
--admin-radius-sm: 8px;
--admin-radius-md: 12px;
--admin-radius-lg: 16px;
--admin-radius-xl: 22px;
--admin-radius-2xl: 28px;
--el-border-radius-base: var(--admin-radius-md);
--el-border-radius-small: var(--admin-radius-sm);
--el-border-radius-round: 999px;
/* Menu */
--el-menu-item-height: 46px;
--el-menu-sub-item-height: var(--el-menu-item-height);
--el-menu-icon-width: 20px;
--el-menu-base-level-padding: 14px;
--el-menu-level-padding: 22px;
/* Surfaces */
--color-white: #ffffff;
--table-header-bg-color: rgba(6, 182, 212, 0.06);
--sidebar-dark-bg: #060d18;
--sidebar-dark-hover: rgba(255, 255, 255, 0.05);
--sidebar-dark-active: rgba(6, 182, 212, 0.14);
--admin-surface-elevated: #ffffff;
--admin-surface-muted: rgba(255, 255, 255, 0.72);
--admin-surface-glass: rgba(255, 255, 255, 0.82);
--el-bg-color: var(--color-white);
--el-bg-color-page: #f6f6f6;
--el-bg-color-page: #eef6fb;
--el-bg-color-overlay: #ffffff;
--el-text-color-primary: #333333;
--el-text-color-regular: #666666;
--el-text-color-secondary: #999999;
--el-text-color-placeholder: #a8abb2;
--el-text-color-disabled: #c0c4cc;
--el-border-color: #dcdfe6;
--el-border-color-light: #e4e7ed;
--el-border-color-lighter: #ebeef5;
--el-border-color-extra-light: #f2f2f2;
--el-border-color-dark: #d4d7de;
--el-border-color-darker: #cdd0d6;
--el-fill-color: #f0f2f5;
--el-fill-color-light: #f8f8f8;
--el-fill-color-lighter: #fafafa;
--el-fill-color-extra-light: #fafcff;
--el-fill-color-dark: #ebedf0;
--el-fill-color-darker: #e6e8eb;
--el-text-color-primary: #0b1220;
--el-text-color-regular: #334155;
--el-text-color-secondary: #64748b;
--el-text-color-placeholder: #94a3b8;
--el-text-color-disabled: #cbd5e1;
--el-border-color: rgba(6, 182, 212, 0.12);
--el-border-color-light: rgba(6, 182, 212, 0.08);
--el-border-color-lighter: rgba(15, 23, 42, 0.06);
--el-border-color-extra-light: rgba(15, 23, 42, 0.04);
--el-border-color-dark: #cbd5e1;
--el-border-color-darker: #94a3b8;
--el-fill-color: #f1f5f9;
--el-fill-color-light: #f8fafc;
--el-fill-color-lighter: #fafbfc;
--el-fill-color-extra-light: #fcfdfe;
--el-fill-color-dark: #e2e8f0;
--el-fill-color-darker: #cbd5e1;
--el-fill-color-blank: #ffffff;
/* 过亮会盖住抽屉/弹窗下的内容;Element Loading 与部分蒙层共用此变量 */
--el-mask-color: rgba(255, 255, 255, 0.5);
--el-mask-color-extra-light: rgba(255, 255, 255, 0.22);
-el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.04), 0px 8px 20px rgba(0, 0, 0, 0.08);
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.12);
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.12);
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.08), 0px 12px 32px rgba(0, 0, 0, 0.12),
0px 8px 16px -8px rgba(0, 0, 0, 0.16);
--el-mask-color: rgba(6, 13, 24, 0.55);
--el-mask-color-extra-light: rgba(6, 13, 24, 0.1);
--el-box-shadow: 0 4px 24px rgba(6, 182, 212, 0.08), 0 12px 40px rgba(15, 23, 42, 0.06);
--el-box-shadow-light: 0 2px 16px rgba(6, 182, 212, 0.1);
--el-box-shadow-lighter: 0 1px 4px rgba(15, 23, 42, 0.04);
--el-box-shadow-dark: 0 16px 48px rgba(6, 182, 212, 0.14), 0 32px 64px rgba(15, 23, 42, 0.1);
--admin-header-border: rgba(6, 182, 212, 0.1);
--admin-sidebar-border: rgba(255, 255, 255, 0.06);
--admin-stage-bg: transparent;
}
+25 -6
View File
@@ -1,9 +1,10 @@
<template>
<div class="change-password flex flex-col">
<div class="flex-1 flex items-center justify-center">
<div class="change-password-card bg-body rounded-md px-10 py-10 w-[480px]">
<div class="text-center text-2xl font-medium mb-2">首次登录</div>
<div class="text-center text-gray-500 text-sm mb-8">为了您的账号安全请修改初始密码</div>
<div class="change-password-backdrop" aria-hidden="true"></div>
<div class="flex-1 flex items-center justify-center relative z-[1] px-4">
<div class="change-password-card">
<div class="text-center text-2xl font-semibold mb-2 text-tx-primary">首次登录</div>
<div class="text-center text-tx-secondary text-sm mb-8">为了您的账号安全请修改初始密码</div>
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
<el-form-item prop="password">
@@ -117,7 +118,25 @@ const { isLock, lockFn: lockSubmit } = useLockFn(handleSubmit)
<style lang="scss" scoped>
.change-password {
background-image: url('./images/login_bg.png');
@apply min-h-screen bg-no-repeat bg-center bg-cover;
position: relative;
min-height: 100vh;
background: #0f172a;
}
.change-password-backdrop {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 15% 20%, rgba(15, 118, 110, 0.35), transparent 42%),
linear-gradient(160deg, #0f172a 0%, #111827 48%, #0b1120 100%);
}
.change-password-card {
width: min(480px, 100%);
padding: 40px 36px;
border-radius: var(--admin-radius-xl);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 24px 64px rgba(2, 6, 23, 0.45);
background: var(--el-bg-color);
}
</style>
+192 -32
View File
@@ -1,33 +1,41 @@
<template>
<div class="login flex flex-col">
<div class="flex-1 flex items-center justify-center">
<div class="login-card flex rounded-md overflow-hidden">
<div class="flex-1 h-full hidden md:inline-block">
<image-contain :src="config.login_image" :width="400" height="100%" />
<div class="login-v2">
<div class="login-v2__mesh" aria-hidden="true"></div>
<div class="login-v2__layout">
<aside class="login-v2__brand">
<div class="login-v2__brand-inner">
<div class="login-v2__mark">ZYT</div>
<p class="login-v2__kicker">互联网医院</p>
<h1 class="login-v2__headline">医生与医助<br />智慧工作台</h1>
<p class="login-v2__lead">问诊处方订单与运营数据一屏协同</p>
<div v-if="config.login_image" class="login-v2__visual hidden xl:block">
<image-contain :src="config.login_image" :width="420" height="280" />
</div>
</div>
<div
class="login-form bg-body flex flex-col justify-center px-10 py-10 md:w-[420px] w-[380px] flex-none mx-auto"
>
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
</aside>
<!-- 企业微信自动授权中 -->
<div v-if="wxWorkAutoLogin" class="text-center py-10">
<section class="login-v2__panel">
<div class="login-v2__glass">
<div class="login-v2__form-head">
<h2 class="login-v2__form-title">{{ config.web_name }}</h2>
<p class="login-v2__form-sub">登录后继续你的接诊与管理工作</p>
</div>
<div v-if="wxWorkAutoLogin" class="login-v2__loading">
<el-icon class="is-loading mb-4" :size="40" color="var(--el-color-primary)">
<Loading />
</el-icon>
<div class="text-gray-500">企业微信授权登录中...</div>
<div class="text-tx-secondary">企业微信授权登录中...</div>
</div>
<template v-else>
<!-- 登录方式切换标签 -->
<div v-if="wxWorkEnabled" class="flex justify-center mb-6">
<div v-if="wxWorkEnabled" class="login-v2__mode">
<el-radio-group v-model="loginMode" size="large">
<el-radio-button value="account">账号登录</el-radio-button>
<el-radio-button value="wxwork">企业微信</el-radio-button>
</el-radio-group>
</div>
<!-- 账号密码登录 -->
<template v-if="loginMode === 'account'">
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
<el-form-item prop="account">
@@ -58,29 +66,34 @@
<div class="mb-5">
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
</div>
<el-button type="primary" size="large" :loading="isLock" @click="lockLogin">
登录
<el-button
class="login-v2__submit"
type="primary"
size="large"
:loading="isLock"
@click="lockLogin"
>
进入工作台
</el-button>
</template>
<!-- 企业微信扫码登录非企业微信内浏览器 -->
<template v-if="loginMode === 'wxwork'">
<div class="wxwork-qrcode-wrap">
<div v-if="wxWorkLoading" class="text-center py-10">
<el-icon class="is-loading" :size="32" color="var(--el-color-primary)">
<Loading />
</el-icon>
<div class="mt-2 text-gray-400 text-sm">加载企业微信扫码...</div>
<div class="mt-2 text-tx-secondary text-sm">加载企业微信扫码...</div>
</div>
<div v-else id="wxwork_qrcode_container" class="wxwork-qrcode"></div>
</div>
<div class="text-center text-sm text-gray-400 mt-4">
<div class="text-center text-sm text-tx-secondary mt-4">
请使用企业微信扫描二维码登录
</div>
</template>
</template>
</div>
</div>
</section>
</div>
<layout-footer />
</div>
@@ -286,31 +299,178 @@ onMounted(async () => {
</script>
<style lang="scss" scoped>
.login {
background-image: url('./images/login_bg.png');
@apply min-h-screen bg-no-repeat bg-center bg-cover;
.login-card {
height: auto;
min-height: 400px;
.login-v2 {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
background: #030712;
overflow: hidden;
}
.login-v2__mesh {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 12% 18%, rgba(6, 182, 212, 0.35), transparent 42%),
radial-gradient(circle at 88% 12%, rgba(16, 185, 129, 0.28), transparent 38%),
radial-gradient(circle at 70% 88%, rgba(99, 102, 241, 0.18), transparent 40%),
linear-gradient(155deg, #030712 0%, #0b1220 45%, #060d18 100%);
}
.login-v2__layout {
position: relative;
z-index: 1;
flex: 1;
display: grid;
grid-template-columns: 1fr;
min-height: calc(100vh - 48px);
}
@media (min-width: 1024px) {
.login-v2__layout {
grid-template-columns: minmax(0, 1.05fr) minmax(420px, 520px);
}
}
.login-v2__brand {
display: none;
padding: 48px 56px;
color: #ffffff;
}
@media (min-width: 1024px) {
.login-v2__brand {
display: flex;
align-items: center;
}
}
.login-v2__brand-inner {
max-width: 520px;
}
.login-v2__mark {
width: 48px;
height: 48px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.1em;
background: var(--admin-brand-gradient);
box-shadow: var(--admin-brand-glow);
margin-bottom: 28px;
}
.login-v2__kicker {
margin: 0 0 12px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.72);
}
.login-v2__headline {
margin: 0;
font-size: clamp(32px, 4vw, 44px);
font-weight: 800;
line-height: 1.15;
letter-spacing: -0.03em;
}
.login-v2__lead {
margin: 16px 0 0;
font-size: 16px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.72);
max-width: 36ch;
}
.login-v2__visual {
margin-top: 36px;
border-radius: var(--admin-radius-xl);
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35);
}
.login-v2__panel {
display: flex;
align-items: center;
justify-content: center;
padding: 32px 20px;
}
.login-v2__glass {
width: min(440px, 100%);
padding: 36px 32px;
border-radius: var(--admin-radius-2xl);
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(24px) saturate(160%);
box-shadow: var(--el-box-shadow-dark);
}
.login-v2__form-head {
margin-bottom: 28px;
}
.login-v2__form-title {
margin: 0;
font-size: 26px;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--el-text-color-primary);
}
.login-v2__form-sub {
margin: 8px 0 0;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.login-v2__mode {
display: flex;
justify-content: center;
margin-bottom: 24px;
}
.login-v2__submit {
width: 100%;
}
.login-v2__loading {
text-align: center;
padding: 40px 0;
}
.wxwork-qrcode-wrap {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
min-height: 360px;
}
.wxwork-qrcode {
width: 340px;
height: 400px;
width: 320px;
height: 360px;
overflow: hidden;
:deep(iframe) {
width: 340px !important;
height: 400px !important;
width: 320px !important;
height: 360px !important;
border: none;
}
}
@media (prefers-reduced-transparency: reduce) {
.login-v2__glass {
background: #ffffff;
backdrop-filter: none;
}
}
</style>
+9 -9
View File
@@ -1,17 +1,17 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-alert
type="warning"
title="温馨提示:用于管理网站的分类,只可添加到一级"
:closable="false"
show-icon
/>
</el-card>
<el-card class="!border-none mt-4" shadow="never" v-loading="pager.loading">
<div>
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button
class="mb-4"
v-perms="['article.articleCate/add']"
type="primary"
@click="handleAdd()"
@@ -21,7 +21,7 @@
</template>
新增
</el-button>
</div>
</template>
<el-table size="large" :data="pager.lists">
<el-table-column label="栏目名称" prop="name" min-width="120" />
<el-table-column label="文章数" prop="article_count" min-width="120" />
@@ -58,10 +58,10 @@
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
+12 -11
View File
@@ -1,6 +1,6 @@
<template>
<div class="article-lists">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="文章标题">
<el-input
@@ -33,24 +33,25 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<router-link
v-perms="['article.article/add', 'article.article/add:edit']"
:to="{
path: getRoutePath('article.article/add:edit')
}"
>
<el-button type="primary" class="mb-4">
<el-button type="primary">
<template #icon>
<icon name="el-icon-Plus" />
</template>
发布文章
</el-button>
</router-link>
</div>
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
</template>
<el-table size="large" :data="pager.lists">
<el-table-column label="ID" prop="id" min-width="80" />
<el-table-column label="封面" min-width="100">
<template #default="{ row }">
@@ -116,10 +117,10 @@
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
</div>
</template>
<script lang="ts" setup name="articleLists">
+10 -10
View File
@@ -1,7 +1,7 @@
<template>
<div class="asset-resource-container">
<!-- 搜索区域 -->
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-form class="ls-form" :model="searchData" inline>
<el-form-item class="w-[280px]" label="标题">
<el-input
@@ -28,25 +28,25 @@
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</admin-page-filter-panel>
<!-- 列表区域 -->
<el-card class="!border-none mt-4" shadow="never">
<div class="mb-4 flex items-center gap-2">
<admin-page-data-panel v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="handleAdd">上传资源</el-button>
<el-button type="danger" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
批量删除
</el-button>
<span v-if="selectedIds.length > 0" class="text-sm text-gray-500">已选 {{ selectedIds.length }} </span>
</div>
</template>
<el-tabs v-model="queryParams.type" @tab-change="handleTabChange">
<el-tab-pane label="图片" name="1"></el-tab-pane>
<el-tab-pane label="视频" name="2"></el-tab-pane>
<el-tab-pane label="语音" name="3"></el-tab-pane>
</el-tabs>
<el-table :data="tableData" v-loading="loading" @selection-change="handleSelectionChange">
<el-table :data="tableData" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
<el-table-column prop="title" label="标题" min-width="80" />
@@ -71,7 +71,7 @@
</el-table-column>
</el-table>
<div class="mt-4 flex justify-end">
<template #footer>
<el-pagination
v-model:current-page="queryParams.page_no"
v-model:page-size="queryParams.page_size"
@@ -80,8 +80,8 @@
@size-change="getList"
@current-change="getList"
/>
</div>
</el-card>
</template>
</admin-page-data-panel>
<!-- 新增/编辑资源弹窗 -->
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑资源' : '新增分发资源'" width="600px" destroy-on-close>
+10 -10
View File
@@ -1,7 +1,7 @@
<template>
<div class="asset-user-container">
<!-- 搜索区域 -->
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-form class="ls-form" :model="searchData" inline>
<el-form-item class="w-[280px]" label="手机号">
<el-input
@@ -16,15 +16,15 @@
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</admin-page-filter-panel>
<!-- 列表区域 -->
<el-card class="!border-none mt-4" shadow="never">
<div class="mb-4">
<admin-page-data-panel v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="handleAdd">新增账号</el-button>
</div>
<el-table :data="tableData" v-loading="loading">
</template>
<el-table :data="tableData">
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
<el-table-column prop="phone" label="手机号" />
<el-table-column label="备注" min-width="200">
@@ -63,7 +63,7 @@
</el-table-column>
</el-table>
<div class="mt-4 flex justify-end">
<template #footer>
<el-pagination
v-model:current-page="queryParams.page_no"
v-model:page-size="queryParams.page_size"
@@ -72,8 +72,8 @@
@size-change="getList"
@current-change="getList"
/>
</div>
</el-card>
</template>
</admin-page-data-panel>
<!-- 编辑/新增弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close>
+7 -6
View File
@@ -1,6 +1,6 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="用户信息">
<el-input
@@ -37,8 +37,9 @@
/>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
</admin-page-filter-panel>
<admin-page-data-panel>
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
<el-table-column label="头像" min-width="100">
<template #default="{ row }">
@@ -67,10 +68,10 @@
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
</div>
</template>
<script lang="ts" setup name="consumerLists">
+7 -7
View File
@@ -1,6 +1,6 @@
<template>
<div class="code-generation">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-form class="mb-[-16px]" :model="formData" inline>
<el-form-item class="w-[280px]" label="表名称">
<el-input v-model="formData.table_name" clearable @keyup.enter="resetPage" />
@@ -13,8 +13,8 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never" v-loading="pager.loading">
</admin-page-filter-panel>
<admin-page-data-panel>
<div class="flex">
<data-table
v-perms="['tools.generator/selectTable']"
@@ -126,10 +126,10 @@
</el-table-column>
</el-table>
</div>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<code-preview
v-if="previewState.show"
v-model="previewState.show"
+1 -1
View File
@@ -866,7 +866,7 @@ onUnmounted(() => {
font-size: 15px;
font-weight: 700;
color: #fff;
background: linear-gradient(145deg, #6366f1 0%, #8b5cf6 100%);
background: linear-gradient(145deg, #0d9488 0%, #0891b2 100%);
flex-shrink: 0;
}
+33 -11
View File
@@ -1,11 +1,11 @@
<template>
<div class="error">
<div>
<div class="error-panel">
<slot name="content">
<div class="error-code">{{ code }}</div>
</slot>
<div class="text-lg text-tx-secondary mt-7 mb-7">{{ title }}</div>
<el-button v-if="showBtn" type="primary" @click="router.go(-1)">
<div class="error-title">{{ title }}</div>
<el-button v-if="showBtn" type="primary" size="large" @click="router.go(-1)">
{{ second }} 秒后返回上一页
</el-button>
</div>
@@ -43,16 +43,38 @@ onUnmounted(() => {
<style lang="scss" scoped>
.error {
text-align: center;
height: 100vh;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
.error-code {
@apply text-primary;
font-size: 150px;
}
.el-button {
width: 176px;
}
background: var(--el-bg-color-page);
padding: 24px;
}
.error-panel {
width: min(480px, 100%);
padding: 48px 32px;
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
background: var(--el-bg-color);
box-shadow: var(--el-box-shadow);
}
.error-code {
@apply text-primary;
font-size: 96px;
font-weight: 700;
line-height: 1;
letter-spacing: -0.04em;
}
.error-title {
@apply text-tx-secondary;
font-size: 18px;
margin: 20px 0 28px;
}
.el-button {
min-width: 176px;
}
</style>
+9 -9
View File
@@ -1,7 +1,7 @@
<template>
<div class="fans-management">
<!-- 搜索区域 -->
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-form class="ls-form" :model="formData" inline>
<el-form-item class="w-[280px]" label="姓名">
<el-input
@@ -36,14 +36,14 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</admin-page-filter-panel>
<!-- 列表区域 -->
<el-card class="!border-none mt-4" shadow="never">
<div class="mb-4">
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button type="primary" @click="handleAdd">新增粉丝</el-button>
</div>
<el-table :data="pager.lists" size="large" v-loading="pager.loading">
</template>
<el-table :data="pager.lists" size="large">
<el-table-column label="ID" prop="id" width="70" />
<el-table-column label="姓名" prop="name" min-width="100" />
<el-table-column label="手机号" prop="phone" min-width="130" />
@@ -81,10 +81,10 @@
</template>
</el-table-column>
</el-table>
<div class="flex mt-4 justify-end">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<!-- 新增/编辑粉丝弹窗 -->
<el-dialog
+3 -3
View File
@@ -3704,7 +3704,7 @@ function getYejiDeptComboOption(tb: YejiTable) {
const cats = rows.map(r =>
r.dept_name.length > 10 ? `${r.dept_name.slice(0, 10)}…` : r.dept_name
)
const palette = ['#64748b', '#38bdf8', '#6366f1']
const palette = ['#64748b', '#38bdf8', '#0d9488']
const series: any[] = [
{
name: '进线',
@@ -4029,8 +4029,8 @@ onMounted(async () => {
.yeji-page {
min-width: 0;
--yj-brand: #6366f1;
--yj-brand-soft: #eef2ff;
--yj-brand: #0d9488;
--yj-brand-soft: #f0fdfa;
--yj-teal: #0ea5e9;
--yj-teal-soft: #f0f9ff;
--yj-accent: #f43f5e;
+7 -7
View File
@@ -1,6 +1,6 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-alert
type="warning"
title="温馨提示:用户账户变动记录"
@@ -38,9 +38,9 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<el-table size="large" :data="pager.lists">
<el-table-column label="用户账号" prop="account" min-width="100" />
<el-table-column label="用户昵称" min-width="160">
<template #default="{ row }">
@@ -71,10 +71,10 @@
<el-table-column label="来源单号" prop="source_sn" min-width="100" />
<el-table-column label="记录时间" prop="create_time" min-width="120" />
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
</div>
</template>
<script lang="ts" setup name="balanceDetail">
+7 -7
View File
@@ -1,6 +1,6 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-alert
type="warning"
title="温馨提示:用户充值记录"
@@ -54,9 +54,9 @@
/>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<el-table size="large" :data="pager.lists">
<el-table-column label="用户信息" min-width="160">
<template #default="{ row }">
<div class="flex items-center">
@@ -104,10 +104,10 @@
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
</div>
</template>
<script lang="ts" setup name="rechargeRecord">
+7 -7
View File
@@ -20,7 +20,7 @@
</div>
</div>
</el-card>
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-form ref="formRef" class="mb-[-16px] mt-[16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="退款单号">
<el-input
@@ -69,8 +69,8 @@
/> -->
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
</admin-page-filter-panel>
<admin-page-data-panel>
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane
v-for="(item, index) in tabLists"
@@ -78,7 +78,7 @@
:name="index"
:key="index"
>
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
<el-table size="large" :data="pager.lists">
<el-table-column label="退款单号" prop="sn" min-width="190" />
<el-table-column label="用户信息" min-width="160">
<template #default="{ row }">
@@ -140,10 +140,10 @@
</el-tab-pane>
</el-tabs>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<refund-log v-model="showRefundLog" :refund-id="selectRefundId" />
</div>
</template>
+5 -5
View File
@@ -1,14 +1,14 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-alert
type="warning"
title="温馨提示:平台配置在各个场景下的通知发送方式和内容模板"
:closable="false"
show-icon
></el-alert>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<el-tabs v-model="tabsActive" @tab-change="getLists">
<el-tab-pane
v-for="(item, index) in tabsMap"
@@ -18,7 +18,7 @@
lazy
></el-tab-pane>
</el-tabs>
<el-table size="large" :data="pager.lists" v-loading="pager.loading">
<el-table size="large" :data="pager.lists" >
<el-table-column label="通知场景" prop="scene_name" min-width="120" />
<el-table-column label="通知类型" prop="type_desc" min-width="160" />
<el-table-column label="短信通知" min-width="80">
@@ -44,7 +44,7 @@
</template>
</el-table-column>
</el-table>
</el-card>
</admin-page-data-panel>
</div>
</template>
<script lang="ts" setup name="notice">
@@ -1,6 +1,6 @@
<template>
<div>
<el-card class="!border-none" shadow="never" v-loading="state.loading">
<admin-page-data-panel v-loading="state.loading">
<el-table size="large" :data="state.lists">
<el-table-column label="短信渠道" prop="name" min-width="120" />
<el-table-column label="状态" min-width="120">
@@ -22,7 +22,7 @@
</template>
</el-table-column>
</el-table>
</el-card>
</admin-page-data-panel>
<edit-popup ref="editRef" @success="getLists" />
</div>
</template>
@@ -33,13 +33,11 @@ import EditPopup from './edit.vue'
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
// 列表数据
const state = reactive({
loading: false,
lists: []
})
// 获取存储引擎列表数据
const getLists = async () => {
try {
state.loading = true
+8 -12
View File
@@ -1,7 +1,7 @@
<!-- 订单列表 -->
<template>
<div class="order-list">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<!-- 搜索表单 -->
<el-form class="ls-form" :model="queryParams" inline>
<el-form-item class="w-[280px]" label="订单号">
@@ -106,10 +106,10 @@
</el-button>
</el-form-item>
</el-form>
</el-card>
</admin-page-filter-panel>
<!-- Tab 筛选 + 今日收益 -->
<el-card class="!border-none mt-4" shadow="never">
<!-- Tab 筛选 + 今日收益 + 数据表格 -->
<admin-page-data-panel v-loading="pager.loading">
<div class="flex items-center justify-between mb-4">
<el-tabs v-model="patientAssociationTab" @tab-change="handleTabChange">
<el-tab-pane label="全部" name="" />
@@ -122,10 +122,7 @@
<span class="text-gray-400">{{ todayRevenue.count }} </span>
</div>
</div>
</el-card>
<!-- 数据表格 -->
<el-card class="!border-none mt-4" shadow="never">
<div v-perms="['order.order/zhipai']" class="mb-3 flex items-center gap-3">
<el-button type="primary" :disabled="!selectedOrderIds.length" @click="openAssignDialog">
将创建人指给医助
@@ -136,7 +133,6 @@
</div>
<el-table
ref="orderTableRef"
v-loading="pager.loading"
:data="pager.lists"
row-key="id"
size="large"
@@ -283,10 +279,10 @@
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<!-- 详情弹窗 -->
<el-dialog
@@ -1,6 +1,6 @@
<template>
<div class="department">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="部门名称" prop="name">
<el-input
@@ -22,22 +22,21 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
</admin-page-filter-panel>
<admin-page-data-panel v-loading="loading">
<template #toolbar>
<el-button v-perms="['dept.dept/add']" type="primary" @click="handleAdd()">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
<el-button @click="handleExpand"> 展开/折叠 </el-button>
</div>
<el-button @click="handleExpand">展开/折叠</el-button>
</template>
<el-table
ref="tableRef"
class="mt-4"
size="large"
v-loading="loading"
:data="lists"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@@ -68,7 +67,6 @@
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" prop="sort" min-width="100" />
<el-table-column label="更新时间" prop="update_time" min-width="180" />
<el-table-column label="操作" width="160" fixed="right">
@@ -101,7 +99,7 @@
</template>
</el-table-column>
</el-table>
</el-card>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
@@ -120,14 +118,18 @@ let isExpand = false
const loading = ref(false)
const lists = ref<any[]>([])
const queryParams = reactive({
status: '',
name: ''
name: '',
status: ''
})
const showEdit = ref(false)
const getLists = async () => {
loading.value = true
lists.value = await deptLists(queryParams)
loading.value = false
try {
lists.value = await deptLists(queryParams)
} finally {
loading.value = false
}
}
const resetParams = () => {
+11 -10
View File
@@ -1,6 +1,6 @@
<template>
<div class="post-lists">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="岗位编码">
<el-input
@@ -31,17 +31,18 @@
/>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button v-perms="['dept.jobs/add']" type="primary" @click="handleAdd()">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
</div>
<el-table class="mt-4" size="large" v-loading="pager.loading" :data="pager.lists">
</template>
<el-table size="large" :data="pager.lists">
<el-table-column label="岗位编码" prop="code" min-width="100" />
<el-table-column label="岗位名称" prop="name" min-width="100" />
<el-table-column label="排序" prop="sort" min-width="100" />
@@ -75,10 +76,10 @@
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
+1 -1
View File
@@ -1275,7 +1275,7 @@ onUnmounted(() => {
.float-action.edit {
color: #fff;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%);
}
.float-action.edit:hover,
+73 -73
View File
@@ -1,6 +1,6 @@
<template>
<div class="admin">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-form class="mb-[-16px]" :model="formData" inline>
<el-form-item class="w-[280px]" label="管理员账号">
<el-input
@@ -40,77 +40,78 @@
/>
</el-form-item>
</el-form>
</el-card>
<el-card v-loading="pager.loading" class="mt-4 !border-none" shadow="never">
<el-button v-perms="['auth.admin/add']" type="primary" @click="handleAdd">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
<div class="mt-4">
<el-table :data="pager.lists" size="large">
<el-table-column label="ID" prop="id" min-width="60" />>
<el-table-column label="头像" min-width="100">
<template #default="{ row }">
<el-avatar :size="50" :src="row.avatar"></el-avatar>
</template>
</el-table-column>
<el-table-column label="账号" prop="account" min-width="100" />
<el-table-column label="名称" prop="name" min-width="100" />
<el-table-column
label="角色"
prop="role_name"
min-width="100"
show-tooltip-when-overflow
/>
<el-table-column
label="部门"
prop="dept_name"
min-width="100"
show-tooltip-when-overflow
/>
<el-table-column label="创建时间" prop="create_time" min-width="180" />
<el-table-column label="最近登录时间" prop="login_time" min-width="180" />
<el-table-column label="最近登录IP" prop="login_ip" min-width="120" />
<el-table-column label="状态" min-width="100" v-perms="['auth.admin/edit']">
<template #default="{ row }">
<el-switch
v-if="row.root != 1"
v-model="row.disable"
:active-value="0"
:inactive-value="1"
@change="changeStatus(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['auth.admin/edit']"
type="primary"
link
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-if="row.root != 1"
v-perms="['auth.admin/delete']"
type="danger"
link
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="flex mt-4 justify-end">
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button v-perms="['auth.admin/add']" type="primary" @click="handleAdd">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
</template>
<el-table :data="pager.lists" size="large">
<el-table-column label="ID" prop="id" min-width="60" />
<el-table-column label="头像" min-width="100">
<template #default="{ row }">
<el-avatar :size="50" :src="row.avatar"></el-avatar>
</template>
</el-table-column>
<el-table-column label="账号" prop="account" min-width="100" />
<el-table-column label="名称" prop="name" min-width="100" />
<el-table-column
label="角色"
prop="role_name"
min-width="100"
show-tooltip-when-overflow
/>
<el-table-column
label="部门"
prop="dept_name"
min-width="100"
show-tooltip-when-overflow
/>
<el-table-column label="创建时间" prop="create_time" min-width="180" />
<el-table-column label="最近登录时间" prop="login_time" min-width="180" />
<el-table-column label="最近登录IP" prop="login_ip" min-width="120" />
<el-table-column label="状态" min-width="100" v-perms="['auth.admin/edit']">
<template #default="{ row }">
<el-switch
v-if="row.root != 1"
v-model="row.disable"
:active-value="0"
:inactive-value="1"
@change="changeStatus(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['auth.admin/edit']"
type="primary"
link
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-if="row.root != 1"
v-perms="['auth.admin/delete']"
type="danger"
link
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
@@ -125,7 +126,6 @@ import feedback from '@/utils/feedback'
import EditPopup from './edit.vue'
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
// 表单数据
const formData = reactive({
account: '',
name: '',
+6 -8
View File
@@ -1,20 +1,18 @@
<template>
<div class="menu-lists">
<el-card class="!border-none" shadow="never">
<div>
<div>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button v-perms="['auth.menu/add']" type="primary" @click="handleAdd()">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
<el-button @click="handleExpand"> 展开/折叠 </el-button>
</div>
<el-button @click="handleExpand">展开/折叠</el-button>
</template>
<el-table
ref="tableRef"
class="mt-4"
size="large"
v-loading="pager.loading"
:data="pager.lists"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@@ -87,7 +85,7 @@
</template>
</el-table-column>
</el-table>
</el-card>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
+54 -59
View File
@@ -1,68 +1,64 @@
<template>
<div class="role-lists">
<el-card class="!border-none" shadow="never">
<div>
<div>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button v-perms="['auth.role/add']" type="primary" @click="handleAdd">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
</div>
<div class="mt-4" v-loading="pager.loading">
<div>
<el-table :data="pager.lists" size="large">
<el-table-column prop="id" label="ID" min-width="100" />
<el-table-column prop="name" label="名称" min-width="150" />
<el-table-column
prop="desc"
label="备注"
min-width="150"
show-overflow-tooltip
/>
<el-table-column prop="sort" label="排序" min-width="100" />
<el-table-column label="数据范围" min-width="140">
<template #default="{ row }">
{{ dataScopeLabel(row.data_scope) }}
</template>
</el-table-column>
<el-table-column prop="num" label="管理员人数" min-width="100" />
<el-table-column prop="create_time" label="创建时间" min-width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
link
type="primary"
v-perms="['auth.role/edit']"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
link
type="primary"
v-perms="['auth.role/edit']"
@click="handleAuth(row)"
>
分配权限
</el-button>
<el-button
v-perms="['auth.role/delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</div>
</el-card>
</template>
<el-table :data="pager.lists" size="large">
<el-table-column prop="id" label="ID" min-width="100" />
<el-table-column prop="name" label="名称" min-width="150" />
<el-table-column
prop="desc"
label="备注"
min-width="150"
show-overflow-tooltip
/>
<el-table-column prop="sort" label="排序" min-width="100" />
<el-table-column label="数据范围" min-width="140">
<template #default="{ row }">
{{ dataScopeLabel(row.data_scope) }}
</template>
</el-table-column>
<el-table-column prop="num" label="管理员人数" min-width="100" />
<el-table-column prop="create_time" label="创建时间" min-width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
link
type="primary"
v-perms="['auth.role/edit']"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
link
type="primary"
v-perms="['auth.role/edit']"
@click="handleAuth(row)"
>
分配权限
</el-button>
<el-button
v-perms="['auth.role/delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<pagination v-model="pager" @change="getLists" />
</template>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
<auth-popup v-if="showAuth" ref="authRef" @success="getLists" @close="showAuth = false" />
</div>
@@ -115,7 +111,6 @@ const dataScopeLabel = (scope: number | string | null | undefined) => {
)
}
// 删除角色
const handleDelete = async (id: number) => {
await feedback.confirm('确定要删除?')
await roleDelete({ id })
+54 -57
View File
@@ -1,6 +1,6 @@
<template>
<div class="dict-type">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-page-header class="mb-4" content="数据管理" @back="$router.back()" />
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" inline>
<el-form-item class="w-[280px]" label="字典名称">
@@ -28,9 +28,10 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button
v-perms="['setting.dict.dict_data/add']"
type="primary"
@@ -52,58 +53,54 @@
</template>
删除
</el-button>
</div>
<div class="mt-4" v-loading="pager.loading">
<div>
<el-table
:data="pager.lists"
size="large"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="ID" prop="id" />
<el-table-column label="数据名称" prop="name" min-width="120" />
<el-table-column label="数据值" prop="value" min-width="120" />
<el-table-column label="状态">
<template v-slot="{ row }">
<el-tag v-if="row.status == 1">正常</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column
label="备注"
prop="remark"
min-width="120"
show-tooltip-when-overflow
/>
<el-table-column label="排序" prop="sort" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['setting.dict.dict_data/edit']"
link
type="primary"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-perms="['setting.dict.dict_data/delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</div>
</el-card>
</template>
<el-table
:data="pager.lists"
size="large"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="ID" prop="id" />
<el-table-column label="数据名称" prop="name" min-width="120" />
<el-table-column label="数据值" prop="value" min-width="120" />
<el-table-column label="状态">
<template v-slot="{ row }">
<el-tag v-if="row.status == 1">正常</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column
label="备注"
prop="remark"
min-width="120"
show-tooltip-when-overflow
/>
<el-table-column label="排序" prop="sort" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['setting.dict.dict_data/edit']"
link
type="primary"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-perms="['setting.dict.dict_data/delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<pagination v-model="pager" @change="getLists" />
</template>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
+65 -69
View File
@@ -1,6 +1,6 @@
<template>
<div class="dict-type">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" inline>
<el-form-item class="w-[280px]" label="字典名称">
<el-input v-model="queryParams.name" clearable @keyup.enter="resetPage" />
@@ -20,9 +20,10 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button
v-perms="['setting.dict.dict_type/add']"
type="primary"
@@ -44,69 +45,65 @@
</template>
删除
</el-button>
</div>
<div class="mt-4" v-loading="pager.loading">
<div>
<el-table
:data="pager.lists"
size="large"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="ID" prop="id" />
<el-table-column label="字典名称" prop="name" min-width="120" />
<el-table-column label="字典类型" prop="type" min-width="120" />
<el-table-column label="状态">
<template v-slot="{ row }">
<el-tag v-if="row.status == 1">正常</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" show-tooltip-when-overflow />
<el-table-column label="创建时间" prop="create_time" min-width="180" />
<el-table-column label="操作" width="190" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['setting.dict.dict_type/edit']"
link
type="primary"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-perms="['setting.dict.dict_data/lists']"
type="primary"
link
>
<router-link
:to="{
path: getRoutePath('setting.dict.dict_data/lists'),
query: {
id: row.id
}
}"
>
数据管理
</router-link>
</el-button>
<el-button
v-perms="['setting.dict.dict_type/delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</div>
</el-card>
</template>
<el-table
:data="pager.lists"
size="large"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="ID" prop="id" />
<el-table-column label="字典名称" prop="name" min-width="120" />
<el-table-column label="字典类型" prop="type" min-width="120" />
<el-table-column label="状态">
<template v-slot="{ row }">
<el-tag v-if="row.status == 1">正常</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" show-tooltip-when-overflow />
<el-table-column label="创建时间" prop="create_time" min-width="180" />
<el-table-column label="操作" width="190" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['setting.dict.dict_type/edit']"
link
type="primary"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-perms="['setting.dict.dict_data/lists']"
type="primary"
link
>
<router-link
:to="{
path: getRoutePath('setting.dict.dict_data/lists'),
query: {
id: row.id
}
}"
>
数据管理
</router-link>
</el-button>
<el-button
v-perms="['setting.dict.dict_type/delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<pagination v-model="pager" @change="getLists" />
</template>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
@@ -151,7 +148,6 @@ const handleEdit = async (data: any) => {
editRef.value?.setFormData(data)
}
// 删除角色
const handleDelete = async (id: any[] | number) => {
await feedback.confirm('确定要删除?')
await dictTypeDelete({ id })
+2 -2
View File
@@ -1,6 +1,6 @@
<template>
<div class="conversion-stats-page">
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-form :inline="true" :model="queryParams" class="stats-filter-form">
<el-form-item label="统计维度">
<el-segmented
@@ -89,7 +89,7 @@
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</admin-page-filter-panel>
<div class="stats-kpi-grid">
<div
+103 -68
View File
@@ -18,7 +18,7 @@
</span>
<div>
<div class="wb-section-name">订单统计</div>
<div class="wb-section-sub">{{ orderStatsDateRangeText }} · {{ orderStatsData.order_type_name || '' }}</div>
<div class="wb-section-sub">{{ orderStatsDateRangeText }} · {{ orderStatsData.order_type_name || '-' }}</div>
</div>
</div>
<div class="wb-toolbar">
@@ -821,8 +821,8 @@ onMounted(() => {
<style lang="scss" scoped>
.workbench-page {
min-height: 100%;
padding: 20px 20px 40px;
background: linear-gradient(160deg, #eef2ff 0%, #f8fafc 38%, #f1f5f9 100%);
padding: 4px 0 24px;
background: transparent;
}
.wb-hero {
@@ -830,56 +830,86 @@ onMounted(() => {
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
padding: 22px 26px;
border-radius: 20px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.65) 100%);
border: 1px solid rgba(255, 255, 255, 0.95);
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.06);
gap: 20px;
margin-bottom: 24px;
padding: 28px 28px;
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
background: var(--admin-surface-glass);
backdrop-filter: blur(16px);
box-shadow: var(--el-box-shadow-light);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(120deg, rgba(6, 182, 212, 0.14) 0%, transparent 45%),
linear-gradient(300deg, rgba(16, 185, 129, 0.1) 0%, transparent 40%);
pointer-events: none;
}
&::after {
content: '';
position: absolute;
inset: 0 0 auto;
height: 3px;
background: var(--admin-brand-gradient);
}
}
.wb-hero-text {
position: relative;
z-index: 1;
}
.wb-hero-title {
margin: 0 0 6px;
font-size: 26px;
font-weight: 700;
letter-spacing: 0.02em;
color: #0f172a;
margin: 0 0 8px;
font-size: 28px;
font-weight: 800;
letter-spacing: -0.03em;
background: var(--admin-brand-gradient);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.wb-hero-desc {
margin: 0;
font-size: 14px;
color: #64748b;
font-size: 15px;
color: var(--el-text-color-secondary);
}
.wb-refresh-btn {
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.25);
position: relative;
z-index: 1;
box-shadow: var(--admin-brand-glow);
}
.wb-section {
margin-bottom: 20px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 8px 32px rgba(15, 23, 42, 0.05);
border-radius: var(--admin-radius-xl);
background: var(--admin-surface-elevated);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow);
overflow: hidden;
backdrop-filter: blur(10px);
}
.wb-section--diagnosis {
border-top: 3px solid transparent;
border-image: linear-gradient(90deg, #3b82f6, #60a5fa) 1;
border-image: linear-gradient(90deg, #06b6d4, #10b981) 1;
}
.wb-section--order {
border-top: 3px solid transparent;
border-image: linear-gradient(90deg, #8b5cf6, #a78bfa) 1;
border-image: linear-gradient(90deg, #0891b2, #6366f1) 1;
}
.wb-section--trend {
border-top: 3px solid transparent;
border-image: linear-gradient(90deg, #14b8a6, #2dd4bf) 1;
border-image: linear-gradient(90deg, #10b981, #06b6d4) 1;
}
.wb-section-head {
@@ -889,8 +919,8 @@ onMounted(() => {
justify-content: space-between;
gap: 12px;
padding: 18px 22px;
border-bottom: 1px solid rgba(226, 232, 240, 0.9);
background: linear-gradient(180deg, rgba(248, 250, 252, 0.9) 0%, rgba(255, 255, 255, 0) 100%);
border-bottom: 1px solid var(--el-border-color-extra-light);
background: var(--admin-brand-gradient-soft);
}
.wb-section-title {
@@ -903,38 +933,68 @@ onMounted(() => {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
width: 46px;
height: 46px;
border-radius: 14px;
font-size: 22px;
color: #fff;
}
.wb-section-icon--blue {
background: linear-gradient(135deg, #3b82f6, #2563eb);
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.35);
background: var(--admin-brand-gradient);
box-shadow: var(--admin-brand-glow);
}
.wb-section-icon--violet {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
box-shadow: 0 8px 20px rgba(124, 58, 237, 0.3);
background: linear-gradient(135deg, #0891b2, #6366f1);
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25);
}
.wb-section-icon--teal {
background: linear-gradient(135deg, #14b8a6, #0d9488);
box-shadow: 0 8px 20px rgba(13, 148, 136, 0.3);
background: linear-gradient(135deg, #10b981, #06b6d4);
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.25);
}
.wb-kpi--blue {
background: linear-gradient(145deg, rgba(6, 182, 212, 0.12), rgba(16, 185, 129, 0.06));
border: 1px solid rgba(6, 182, 212, 0.22);
box-shadow: var(--el-box-shadow-lighter);
}
.wb-kpi--violet {
background: linear-gradient(145deg, rgba(99, 102, 241, 0.1), rgba(6, 182, 212, 0.06));
border: 1px solid rgba(99, 102, 241, 0.2);
box-shadow: var(--el-box-shadow-lighter);
}
.wb-kpi--amber {
background: linear-gradient(145deg, rgba(245, 158, 11, 0.12), rgba(251, 191, 36, 0.06));
border: 1px solid rgba(245, 158, 11, 0.24);
box-shadow: var(--el-box-shadow-lighter);
}
.wb-rank-strip {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 112px;
padding: 14px 18px;
border-radius: var(--admin-radius-lg);
background: var(--admin-brand-gradient-soft);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow-lighter);
}
.wb-section-name {
font-size: 17px;
font-size: 16px;
font-weight: 600;
color: #0f172a;
color: var(--el-text-color-primary);
}
.wb-section-sub {
margin-top: 2px;
font-size: 12px;
color: #94a3b8;
color: var(--el-text-color-secondary);
}
.wb-toolbar {
@@ -968,21 +1028,6 @@ onMounted(() => {
box-sizing: border-box;
}
.wb-kpi--blue {
background: linear-gradient(145deg, #eff6ff 0%, #dbeafe 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
}
.wb-kpi--violet {
background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 100%);
border: 1px solid rgba(139, 92, 246, 0.22);
}
.wb-kpi--amber {
background: linear-gradient(145deg, #fffbeb 0%, #fef3c7 100%);
border: 1px solid rgba(245, 158, 11, 0.25);
}
.wb-kpi-label {
font-size: 13px;
color: #64748b;
@@ -1007,17 +1052,6 @@ onMounted(() => {
color: #94a3b8;
}
.wb-rank-strip {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 112px;
padding: 14px 18px;
border-radius: 16px;
background: #f8fafc;
border: 1px solid #e2e8f0;
}
.wb-rank-strip--compact {
min-height: 112px;
}
@@ -1080,9 +1114,10 @@ onMounted(() => {
.wb-chart-card {
height: 100%;
padding: 14px 16px 8px;
border-radius: 16px;
background: #fafbfc;
border: 1px solid #eef0f4;
border-radius: var(--admin-radius-lg);
background: var(--admin-surface-elevated);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow-lighter);
}
.wb-chart-card-head {
+16 -1
View File
@@ -77,7 +77,22 @@ module.exports = {
mask: 'var(--el-mask-color)'
},
fontFamily: {
sans: ['PingFang SC', 'Arial', 'Hiragino Sans GB', 'Microsoft YaHei', 'sans-serif']
sans: [
'PingFang SC',
'SF Pro Text',
'Segoe UI',
'Arial',
'Hiragino Sans GB',
'Microsoft YaHei',
'sans-serif'
]
},
borderRadius: {
sm: 'var(--admin-radius-sm)',
DEFAULT: 'var(--admin-radius-md)',
md: 'var(--admin-radius-md)',
lg: 'var(--admin-radius-lg)',
xl: 'var(--admin-radius-xl)'
},
boxShadow: {
DEFAULT: 'var(--el-box-shadow)',
File diff suppressed because one or more lines are too long