Merge branch 'main' of http://git.tensent.cn/molong/vueadmin
This commit is contained in:
@@ -30,12 +30,13 @@
|
|||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@vitejs/plugin-vue": "^6.0.3",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"sass-embedded": "^1.97.2",
|
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-vue": "~10.6.2",
|
"eslint-plugin-vue": "~10.6.2",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"prettier": "3.7.4",
|
"prettier": "3.7.4",
|
||||||
|
"sass-embedded": "^1.97.2",
|
||||||
"vite": "^7.3.0",
|
"vite": "^7.3.0",
|
||||||
"vite-plugin-vue-devtools": "^8.0.5"
|
"vite-plugin-vue-devtools": "^8.0.5",
|
||||||
|
"vue-eslint-parser": "^10.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/App.vue
59
src/App.vue
@@ -1,17 +1,70 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from 'vue'
|
import { onMounted, computed } from 'vue'
|
||||||
import { useI18nStore } from './stores/modules/i18n'
|
import { useI18nStore } from './stores/modules/i18n'
|
||||||
|
import { useLayoutStore } from './stores/modules/layout'
|
||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
|
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||||
|
import enUS from 'ant-design-vue/es/locale/en_US'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import 'dayjs/locale/zh-cn'
|
||||||
|
import 'dayjs/locale/en'
|
||||||
|
|
||||||
|
// i18n store
|
||||||
|
const i18nStore = useI18nStore()
|
||||||
|
|
||||||
|
// layout store
|
||||||
|
const layoutStore = useLayoutStore()
|
||||||
|
|
||||||
|
// Ant Design Vue 语言配置
|
||||||
|
const antLocale = computed(() => {
|
||||||
|
return i18nStore.currentLocale === 'zh-CN' ? zhCN : enUS
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取弹出容器
|
||||||
|
const getPopupContainer = () => {
|
||||||
|
return document.body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ant Design Vue 主题配置
|
||||||
|
const antdTheme = computed(() => {
|
||||||
|
return {
|
||||||
|
token: {
|
||||||
|
colorPrimary: layoutStore.themeColor,
|
||||||
|
borderRadius: 6,
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Layout: {
|
||||||
|
headerBg: '#fff',
|
||||||
|
siderBg: '#001529',
|
||||||
|
},
|
||||||
|
Menu: {
|
||||||
|
darkItemBg: '#001529',
|
||||||
|
darkItemSelectedBg: '#1890ff',
|
||||||
|
darkItemHoverBg: '#002140',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 从持久化的 store 中读取语言设置并同步到 i18n
|
// 从持久化的 store 中读取语言设置并同步到 i18n
|
||||||
const i18nStore = useI18nStore()
|
|
||||||
i18n.global.locale.value = i18nStore.currentLocale
|
i18n.global.locale.value = i18nStore.currentLocale
|
||||||
|
|
||||||
|
// 同步 dayjs 语言
|
||||||
|
dayjs.locale(i18nStore.currentLocale === 'zh-CN' ? 'zh-cn' : 'en')
|
||||||
|
|
||||||
|
// 初始化主题颜色
|
||||||
|
if (layoutStore.themeColor) {
|
||||||
|
document.documentElement.style.setProperty('--primary-color', layoutStore.themeColor)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-view />
|
<a-config-provider :locale="antLocale" :theme="antdTheme" :getPopupContainer="getPopupContainer">
|
||||||
|
<router-view />
|
||||||
|
</a-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
320
src/components/scForm/index.vue
Normal file
320
src/components/scForm/index.vue
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
<template>
|
||||||
|
<a-form :model="formData" :rules="rules" :label-col="labelCol" :wrapper-col="wrapperCol" :layout="layout"
|
||||||
|
@finish="handleFinish" @finish-failed="handleFinishFailed">
|
||||||
|
<a-form-item v-for="item in formItems" :key="item.field" :label="item.label" :name="item.field"
|
||||||
|
:required="item.required" :colon="item.colon">
|
||||||
|
<!-- 输入框 -->
|
||||||
|
<template v-if="item.type === 'input'">
|
||||||
|
<a-input v-model:value="formData[item.field]" :placeholder="item.placeholder || `请输入${item.label}`"
|
||||||
|
:disabled="item.disabled" :allow-clear="item.allowClear !== false" :max-length="item.maxLength"
|
||||||
|
:type="item.inputType || 'text'" :prefix="item.prefix" :suffix="item.suffix"
|
||||||
|
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 文本域 -->
|
||||||
|
<template v-else-if="item.type === 'textarea'">
|
||||||
|
<a-textarea v-model:value="formData[item.field]" :placeholder="item.placeholder || `请输入${item.label}`"
|
||||||
|
:disabled="item.disabled" :allow-clear="item.allowClear !== false" :rows="item.rows || 4"
|
||||||
|
:max-length="item.maxLength" :show-count="item.showCount"
|
||||||
|
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 密码输入框 -->
|
||||||
|
<template v-else-if="item.type === 'password'">
|
||||||
|
<a-input-password v-model:value="formData[item.field]"
|
||||||
|
:placeholder="item.placeholder || `请输入${item.label}`" :disabled="item.disabled"
|
||||||
|
:max-length="item.maxLength" @change="item.onChange && item.onChange(formData[item.field])" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 数字输入框 -->
|
||||||
|
<template v-else-if="item.type === 'number'">
|
||||||
|
<a-input-number v-model:value="formData[item.field]"
|
||||||
|
:placeholder="item.placeholder || `请输入${item.label}`" :disabled="item.disabled" :min="item.min"
|
||||||
|
:max="item.max" :step="item.step || 1" :precision="item.precision"
|
||||||
|
:controls="item.controls !== false" style="width: 100%"
|
||||||
|
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 下拉选择 -->
|
||||||
|
<template v-else-if="item.type === 'select'">
|
||||||
|
<a-select v-model:value="formData[item.field]" :placeholder="item.placeholder || `请选择${item.label}`"
|
||||||
|
:disabled="item.disabled" :allow-clear="item.allowClear !== false" :mode="item.mode"
|
||||||
|
:options="item.options" :field-names="item.fieldNames" style="width: 100%"
|
||||||
|
@change="item.onChange && item.onChange(formData[item.field])">
|
||||||
|
<template v-if="!item.options" #notFoundContent>
|
||||||
|
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" description="暂无数据" />
|
||||||
|
</template>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 单选框 -->
|
||||||
|
<template v-else-if="item.type === 'radio'">
|
||||||
|
<a-radio-group v-model:value="formData[item.field]" :disabled="item.disabled"
|
||||||
|
:button-style="item.buttonStyle" @change="item.onChange && item.onChange(formData[item.field])">
|
||||||
|
<template v-if="item.options">
|
||||||
|
<a-radio v-for="opt in item.options" :key="opt.value" :value="opt.value"
|
||||||
|
:disabled="opt.disabled">
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-radio>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.buttonStyle === 'solid'">
|
||||||
|
<a-radio-button v-for="opt in item.options" :key="opt.value" :value="opt.value"
|
||||||
|
:disabled="opt.disabled">
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-radio-button>
|
||||||
|
</template>
|
||||||
|
</a-radio-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 多选框 -->
|
||||||
|
<template v-else-if="item.type === 'checkbox'">
|
||||||
|
<a-checkbox-group v-model:value="formData[item.field]" :disabled="item.disabled"
|
||||||
|
@change="item.onChange && item.onChange(formData[item.field])">
|
||||||
|
<template v-if="item.options">
|
||||||
|
<a-checkbox v-for="opt in item.options" :key="opt.value" :value="opt.value"
|
||||||
|
:disabled="opt.disabled">
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-checkbox>
|
||||||
|
</template>
|
||||||
|
</a-checkbox-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 开关 -->
|
||||||
|
<template v-else-if="item.type === 'switch'">
|
||||||
|
<a-switch v-model:checked="formData[item.field]" :disabled="item.disabled"
|
||||||
|
:checked-children="item.checkedChildren || '开'" :un-checked-children="item.unCheckedChildren || '关'"
|
||||||
|
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 日期选择 -->
|
||||||
|
<template v-else-if="item.type === 'date'">
|
||||||
|
<a-date-picker v-model:value="formData[item.field]"
|
||||||
|
:placeholder="item.placeholder || `请选择${item.label}`" :disabled="item.disabled"
|
||||||
|
:format="item.format || 'YYYY-MM-DD'" :value-format="item.valueFormat || 'YYYY-MM-DD'"
|
||||||
|
style="width: 100%" @change="item.onChange && item.onChange(formData[item.field])" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 日期范围选择 -->
|
||||||
|
<template v-else-if="item.type === 'dateRange'">
|
||||||
|
<a-range-picker v-model:value="formData[item.field]" :placeholder="item.placeholder || ['开始日期', '结束日期']"
|
||||||
|
:disabled="item.disabled" :format="item.format || 'YYYY-MM-DD'"
|
||||||
|
:value-format="item.valueFormat || 'YYYY-MM-DD'" style="width: 100%"
|
||||||
|
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 时间选择 -->
|
||||||
|
<template v-else-if="item.type === 'time'">
|
||||||
|
<a-time-picker v-model:value="formData[item.field]"
|
||||||
|
:placeholder="item.placeholder || `请选择${item.label}`" :disabled="item.disabled"
|
||||||
|
:format="item.format || 'HH:mm:ss'" :value-format="item.valueFormat || 'HH:mm:ss'"
|
||||||
|
style="width: 100%" @change="item.onChange && item.onChange(formData[item.field])" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 上传 -->
|
||||||
|
<template v-else-if="item.type === 'upload'">
|
||||||
|
<a-upload v-model:file-list="formData[item.field]" :list-type="item.listType || 'text'"
|
||||||
|
:action="item.action" :max-count="item.maxCount" :before-upload="item.beforeUpload"
|
||||||
|
:custom-request="item.customRequest" :accept="item.accept" :disabled="item.disabled"
|
||||||
|
@change="(info) => item.onChange && item.onChange(info)">
|
||||||
|
<a-button v-if="item.listType !== 'picture-card'" type="primary">
|
||||||
|
<UploadOutlined />
|
||||||
|
点击上传
|
||||||
|
</a-button>
|
||||||
|
<div v-else>
|
||||||
|
<PlusOutlined />
|
||||||
|
<div class="ant-upload-text">上传</div>
|
||||||
|
</div>
|
||||||
|
</a-upload>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 评分 -->
|
||||||
|
<template v-else-if="item.type === 'rate'">
|
||||||
|
<a-rate v-model:value="formData[item.field]" :disabled="item.disabled" :count="item.count || 5"
|
||||||
|
:allow-half="item.allowHalf" @change="item.onChange && item.onChange(formData[item.field])" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 滑块 -->
|
||||||
|
<template v-else-if="item.type === 'slider'">
|
||||||
|
<a-slider v-model:value="formData[item.field]" :disabled="item.disabled" :min="item.min || 0"
|
||||||
|
:max="item.max || 100" :step="item.step || 1" :marks="item.marks" :range="item.range"
|
||||||
|
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 级联选择 -->
|
||||||
|
<template v-else-if="item.type === 'cascader'">
|
||||||
|
<a-cascader v-model:value="formData[item.field]" :options="item.options"
|
||||||
|
:placeholder="item.placeholder || `请选择${item.label}`" :disabled="item.disabled"
|
||||||
|
:change-on-select="item.changeOnSelect" :field-names="item.fieldNames" style="width: 100%"
|
||||||
|
@change="item.onChange && item.onChange(formData[item.field])" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 自定义插槽 -->
|
||||||
|
<template v-else-if="item.type === 'slot'">
|
||||||
|
<slot :name="item.slotName || item.field" :field="item.field" :value="formData[item.field]"></slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 提示信息 -->
|
||||||
|
<template v-if="item.tip">
|
||||||
|
<div class="form-item-tip">{{ item.tip }}</div>
|
||||||
|
</template>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 表单操作按钮 -->
|
||||||
|
<a-form-item v-if="showActions" :wrapper-col="actionWrapperCol">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" html-type="submit" :loading="loading" :size="buttonSize">
|
||||||
|
{{ submitText || '提交' }}
|
||||||
|
</a-button>
|
||||||
|
<a-button v-if="showReset" @click="handleReset" :size="buttonSize">
|
||||||
|
{{ resetText || '重置' }}
|
||||||
|
</a-button>
|
||||||
|
<a-button v-if="showCancel" @click="handleCancel" :size="buttonSize">
|
||||||
|
{{ cancelText || '取消' }}
|
||||||
|
</a-button>
|
||||||
|
<slot name="actions"></slot>
|
||||||
|
</a-space>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 自定义插槽 -->
|
||||||
|
<slot></slot>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, watch } from 'vue'
|
||||||
|
import { Empty } from 'ant-design-vue'
|
||||||
|
import { UploadOutlined, PlusOutlined } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 表单项配置
|
||||||
|
formItems: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// 表单初始值
|
||||||
|
initialValues: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
// 表单布局
|
||||||
|
layout: {
|
||||||
|
type: String,
|
||||||
|
default: 'horizontal', // horizontal, vertical, inline
|
||||||
|
},
|
||||||
|
// 标签宽度
|
||||||
|
labelCol: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ span: 6 }),
|
||||||
|
},
|
||||||
|
// 内容宽度
|
||||||
|
wrapperCol: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ span: 16 }),
|
||||||
|
},
|
||||||
|
// 是否显示操作按钮
|
||||||
|
showActions: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// 操作按钮布局
|
||||||
|
actionWrapperCol: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ offset: 6, span: 16 }),
|
||||||
|
},
|
||||||
|
// 是否显示重置按钮
|
||||||
|
showReset: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// 是否显示取消按钮
|
||||||
|
showCancel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 按钮文字
|
||||||
|
submitText: String,
|
||||||
|
resetText: String,
|
||||||
|
cancelText: String,
|
||||||
|
// 按钮大小
|
||||||
|
buttonSize: {
|
||||||
|
type: String,
|
||||||
|
default: 'middle',
|
||||||
|
},
|
||||||
|
// 加载状态
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['finish', 'finish-failed', 'reset', 'cancel'])
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = reactive({ ...props.initialValues })
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const rules = computed(() => {
|
||||||
|
const result = {}
|
||||||
|
props.formItems.forEach((item) => {
|
||||||
|
if (item.rules && item.rules.length > 0) {
|
||||||
|
result[item.field] = item.rules
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听初始值变化
|
||||||
|
watch(
|
||||||
|
() => props.initialValues,
|
||||||
|
(newVal) => {
|
||||||
|
Object.assign(formData, newVal)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 表单提交
|
||||||
|
const handleFinish = (values) => {
|
||||||
|
emit('finish', values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单验证失败
|
||||||
|
const handleFinishFailed = (errorInfo) => {
|
||||||
|
emit('finish-failed', errorInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const handleReset = () => {
|
||||||
|
Object.assign(formData, props.initialValues)
|
||||||
|
emit('reset', formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消操作
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
formData,
|
||||||
|
resetForm: handleReset,
|
||||||
|
setFieldValue: (field, value) => {
|
||||||
|
formData[field] = value
|
||||||
|
},
|
||||||
|
getFieldValue: (field) => {
|
||||||
|
return formData[field]
|
||||||
|
},
|
||||||
|
setFieldsValue: (values) => {
|
||||||
|
Object.assign(formData, values)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.form-item-tip {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
350
src/components/scTable/index.vue
Normal file
350
src/components/scTable/index.vue
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sc-table-wrapper">
|
||||||
|
<!-- 表格操作栏 -->
|
||||||
|
<div class="table-toolbar" v-if="showToolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<slot name="toolbar-left"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<!-- 列设置 -->
|
||||||
|
<a-dropdown v-if="showColumnSetting" :trigger="['click']">
|
||||||
|
<a-button size="small">
|
||||||
|
<SettingOutlined /> 列设置
|
||||||
|
</a-button>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu @click="handleColumnSetting">
|
||||||
|
<a-menu-item v-for="column in columns" :key="column.key || column.dataIndex">
|
||||||
|
<a-checkbox :checked="visibleColumns.includes(column.key || column.dataIndex)">
|
||||||
|
{{ column.title }}
|
||||||
|
</a-checkbox>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
|
||||||
|
<!-- 刷新按钮 -->
|
||||||
|
<a-button v-if="showRefresh" size="small" @click="handleRefresh">
|
||||||
|
<ReloadOutlined />
|
||||||
|
</a-button>
|
||||||
|
|
||||||
|
<slot name="toolbar-right"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格主体 -->
|
||||||
|
<a-table v-bind="tableProps" :columns="processedColumns" :data-source="dataSource"
|
||||||
|
:pagination="paginationConfig" :loading="loading" :row-selection="rowSelectionConfig" :scroll="scrollConfig"
|
||||||
|
@change="handleTableChange" @row="handleRow" @row-click="handleRowClick" @row-dblclick="handleRowDblClick">
|
||||||
|
<!-- 自定义列插槽将通过customRender在processedColumns中处理 -->
|
||||||
|
|
||||||
|
<!-- 操作列插槽 -->
|
||||||
|
<template v-if="showActionColumn" #action="scope">
|
||||||
|
<slot name="action" v-bind="scope"></slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 空数据提示 -->
|
||||||
|
<template #empty>
|
||||||
|
<a-empty v-if="!error" :description="emptyText" />
|
||||||
|
<div v-else class="table-error">
|
||||||
|
<span>{{ errorMessage }}</span>
|
||||||
|
<a-button size="small" type="link" @click="handleRefresh">重试</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { SettingOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 表格列配置
|
||||||
|
columns: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 数据源
|
||||||
|
dataSource: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 错误状态
|
||||||
|
error: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 错误信息
|
||||||
|
errorMessage: {
|
||||||
|
type: String,
|
||||||
|
default: '加载失败,请稍后重试',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 空数据提示
|
||||||
|
emptyText: {
|
||||||
|
type: String,
|
||||||
|
default: '暂无数据',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
pagination: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
|
showTotal: (total, range) => `共 ${total} 条记录,第 ${range[0]}-${range[1]} 条`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 是否显示分页
|
||||||
|
showPagination: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 行选择配置
|
||||||
|
rowSelection: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
type: 'checkbox',
|
||||||
|
selectedRowKeys: [],
|
||||||
|
onChange: () => { },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 是否显示行选择
|
||||||
|
showRowSelection: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 滚动配置
|
||||||
|
scroll: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 表格属性
|
||||||
|
tableProps: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 操作列配置
|
||||||
|
actionColumn: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 150,
|
||||||
|
fixed: 'right',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 是否显示操作列
|
||||||
|
showActionColumn: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 是否显示工具栏
|
||||||
|
showToolbar: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 是否显示列设置
|
||||||
|
showColumnSetting: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 是否显示刷新按钮
|
||||||
|
showRefresh: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 可见列配置
|
||||||
|
visibleColumns: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['change', 'row-click', 'row-dblclick', 'refresh', 'column-change', 'selection-change', 'page-change', 'size-change', 'sort-change', 'filter-change'])
|
||||||
|
|
||||||
|
// 处理后的列配置
|
||||||
|
const processedColumns = computed(() => {
|
||||||
|
let result = [...props.columns]
|
||||||
|
|
||||||
|
// 过滤可见列
|
||||||
|
if (props.visibleColumns.length > 0) {
|
||||||
|
result = result.filter((column) => props.visibleColumns.includes(column.key || column.dataIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每列添加customRender支持插槽
|
||||||
|
result = result.map((column) => {
|
||||||
|
const columnKey = column.key || column.dataIndex
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
customRender: (_, record, index) => {
|
||||||
|
// 使用渲染函数返回空内容,实际内容通过插槽渲染
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加操作列
|
||||||
|
if (props.showActionColumn) {
|
||||||
|
result.push({
|
||||||
|
...props.actionColumn,
|
||||||
|
customRender: (_, record, index) => {
|
||||||
|
// 使用渲染函数返回空内容,实际内容通过插槽渲染
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const paginationConfig = computed(() => {
|
||||||
|
if (!props.showPagination) return false
|
||||||
|
|
||||||
|
return {
|
||||||
|
...props.pagination,
|
||||||
|
onChange: (page, pageSize) => {
|
||||||
|
emit('page-change', page)
|
||||||
|
emit('change', { current: page, pageSize })
|
||||||
|
},
|
||||||
|
onShowSizeChange: (current, pageSize) => {
|
||||||
|
emit('size-change', pageSize)
|
||||||
|
emit('change', { current, pageSize })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 行选择配置
|
||||||
|
const rowSelectionConfig = computed(() => {
|
||||||
|
if (!props.showRowSelection) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
...props.rowSelection,
|
||||||
|
onChange: (selectedRowKeys, selectedRows) => {
|
||||||
|
emit('selection-change', selectedRowKeys, selectedRows)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 滚动配置
|
||||||
|
const scrollConfig = computed(() => {
|
||||||
|
return {
|
||||||
|
...props.scroll,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理表格变化
|
||||||
|
const handleTableChange = (pagination, filters, sorter, extra) => {
|
||||||
|
if (sorter.field) {
|
||||||
|
emit('sort-change', sorter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(filters).length > 0) {
|
||||||
|
emit('filter-change', filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('change', { pagination, filters, sorter, extra })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理行点击
|
||||||
|
const handleRowClick = (record, event) => {
|
||||||
|
emit('row-click', record, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理行双击
|
||||||
|
const handleRowDblClick = (record, event) => {
|
||||||
|
emit('row-dblclick', record, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理行配置
|
||||||
|
const handleRow = (record) => {
|
||||||
|
return {
|
||||||
|
onClick: (event) => handleRowClick(record, event),
|
||||||
|
onDblclick: (event) => handleRowDblClick(record, event),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理列设置
|
||||||
|
const handleColumnSetting = ({ key, domEvent }) => {
|
||||||
|
const checkbox = domEvent.target.closest('.ant-checkbox-wrapper').querySelector('.ant-checkbox-input')
|
||||||
|
const checked = checkbox.checked
|
||||||
|
|
||||||
|
emit('column-change', {
|
||||||
|
key,
|
||||||
|
visible: checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理刷新
|
||||||
|
const handleRefresh = () => {
|
||||||
|
emit('refresh')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法
|
||||||
|
defineExpose({
|
||||||
|
handleRefresh,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.sc-table-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.table-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.toolbar-left,
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #ff4d4f;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,26 +1,26 @@
|
|||||||
export default {
|
export default {
|
||||||
APP_NAME: 'vueadmin',
|
APP_NAME: 'vueadmin',
|
||||||
DASHBOARD_URL: '/home',
|
DASHBOARD_URL: '/dashboard',
|
||||||
|
|
||||||
// 白名单路由(不需要登录即可访问)
|
// 白名单路由(不需要登录即可访问)
|
||||||
whiteList: ['/login', '/register', '/reset-password'],
|
whiteList: ['/login', '/register', '/reset-password'],
|
||||||
//版本号
|
//版本号
|
||||||
APP_VER: "1.6.6",
|
APP_VER: '1.6.6',
|
||||||
|
|
||||||
//内核版本号
|
//内核版本号
|
||||||
CORE_VER: "1.6.6",
|
CORE_VER: '1.6.6',
|
||||||
|
|
||||||
//接口地址
|
//接口地址
|
||||||
API_URL: "https://www.tensent.cn/admin/",
|
API_URL: 'https://www.tensent.cn/admin/',
|
||||||
|
|
||||||
//请求超时
|
//请求超时
|
||||||
TIMEOUT: 50000,
|
TIMEOUT: 50000,
|
||||||
|
|
||||||
//TokenName
|
//TokenName
|
||||||
TOKEN_NAME: "authorization",
|
TOKEN_NAME: 'authorization',
|
||||||
|
|
||||||
//Token前缀,注意最后有个空格,如不需要需设置空字符串
|
//Token前缀,注意最后有个空格,如不需要需设置空字符串
|
||||||
TOKEN_PREFIX: "Bearer ",
|
TOKEN_PREFIX: 'Bearer ',
|
||||||
|
|
||||||
//追加其他头
|
//追加其他头
|
||||||
HEADERS: {},
|
HEADERS: {},
|
||||||
@@ -28,24 +28,24 @@ export default {
|
|||||||
//请求是否开启缓存
|
//请求是否开启缓存
|
||||||
REQUEST_CACHE: false,
|
REQUEST_CACHE: false,
|
||||||
//语言
|
//语言
|
||||||
LANG: "zh-cn",
|
LANG: 'zh-cn',
|
||||||
|
|
||||||
//是否加密localStorage, 为空不加密
|
//是否加密localStorage, 为空不加密
|
||||||
//支持多种加密方式: 'AES', 'BASE64', 'DES'
|
//支持多种加密方式: 'AES', 'BASE64', 'DES'
|
||||||
LS_ENCRYPTION: "",
|
LS_ENCRYPTION: '',
|
||||||
|
|
||||||
//localStorage加密秘钥,位数建议填写8的倍数
|
//localStorage加密秘钥,位数建议填写8的倍数
|
||||||
LS_ENCRYPTION_key: "2XNN4K8LC0ELVWN4",
|
LS_ENCRYPTION_key: '2XNN4K8LC0ELVWN4',
|
||||||
|
|
||||||
//localStorage加密模式,AES支持: 'ECB', 'CBC', 'CTR', 'OFB', 'CFB'
|
//localStorage加密模式,AES支持: 'ECB', 'CBC', 'CTR', 'OFB', 'CFB'
|
||||||
LS_ENCRYPTION_mode: "ECB",
|
LS_ENCRYPTION_mode: 'ECB',
|
||||||
|
|
||||||
//localStorage加密填充方式,AES支持: 'Pkcs7', 'ZeroPadding', 'Iso10126', 'Iso97971'
|
//localStorage加密填充方式,AES支持: 'Pkcs7', 'ZeroPadding', 'Iso10126', 'Iso97971'
|
||||||
LS_ENCRYPTION_padding: "Pkcs7",
|
LS_ENCRYPTION_padding: 'Pkcs7',
|
||||||
|
|
||||||
//localStorage默认过期时间(单位:小时),0表示永不过期
|
//localStorage默认过期时间(单位:小时),0表示永不过期
|
||||||
LS_DEFAULT_EXPIRE: 720, // 30天
|
LS_DEFAULT_EXPIRE: 720, // 30天
|
||||||
|
|
||||||
//DES加密秘钥,必须是8字节
|
//DES加密秘钥,必须是8字节
|
||||||
LS_DES_key: "12345678",
|
LS_DES_key: '12345678',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,55 +2,6 @@
|
|||||||
* 静态路由配置
|
* 静态路由配置
|
||||||
* 这些路由会根据用户角色进行过滤后添加到路由中
|
* 这些路由会根据用户角色进行过滤后添加到路由中
|
||||||
*/
|
*/
|
||||||
const userRoutes = [
|
const userRoutes = []
|
||||||
{
|
|
||||||
path: '/home',
|
|
||||||
name: 'Home',
|
|
||||||
component: 'home',
|
|
||||||
meta: {
|
|
||||||
title: '首页',
|
|
||||||
icon: 'HomeOutlined',
|
|
||||||
affix: true,
|
|
||||||
noCache: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/system',
|
|
||||||
name: 'System',
|
|
||||||
meta: {
|
|
||||||
title: '系统管理',
|
|
||||||
icon: 'SettingOutlined'
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '/system/user',
|
|
||||||
name: 'SystemUser',
|
|
||||||
component: 'system/user',
|
|
||||||
meta: {
|
|
||||||
title: '用户管理',
|
|
||||||
icon: 'UserOutlined'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/system/role',
|
|
||||||
name: 'SystemRole',
|
|
||||||
component: 'system/role',
|
|
||||||
meta: {
|
|
||||||
title: '角色管理',
|
|
||||||
icon: 'TeamOutlined'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/system/menu',
|
|
||||||
name: 'SystemMenu',
|
|
||||||
component: 'system/menu',
|
|
||||||
meta: {
|
|
||||||
title: '菜单管理',
|
|
||||||
icon: 'MenuOutlined'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export default userRoutes
|
export default userRoutes
|
||||||
|
|||||||
@@ -4,13 +4,9 @@
|
|||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-title">布局模式</div>
|
<div class="setting-title">布局模式</div>
|
||||||
<div class="layout-mode-list">
|
<div class="layout-mode-list">
|
||||||
<div
|
<div v-for="mode in layoutModes" :key="mode.value" class="layout-mode-item"
|
||||||
v-for="mode in layoutModes"
|
|
||||||
:key="mode.value"
|
|
||||||
class="layout-mode-item"
|
|
||||||
:class="{ active: layoutStore.layoutMode === mode.value }"
|
:class="{ active: layoutStore.layoutMode === mode.value }"
|
||||||
@click="handleLayoutChange(mode.value)"
|
@click="handleLayoutChange(mode.value)">
|
||||||
>
|
|
||||||
<div class="layout-preview" :class="`preview-${mode.value}`">
|
<div class="layout-preview" :class="`preview-${mode.value}`">
|
||||||
<div class="preview-sidebar"></div>
|
<div class="preview-sidebar"></div>
|
||||||
<div v-if="mode.value === 'default'" class="preview-sidebar-2"></div>
|
<div v-if="mode.value === 'default'" class="preview-sidebar-2"></div>
|
||||||
@@ -28,14 +24,9 @@
|
|||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-title">主题颜色</div>
|
<div class="setting-title">主题颜色</div>
|
||||||
<div class="color-list">
|
<div class="color-list">
|
||||||
<div
|
<div v-for="color in themeColors" :key="color" class="color-item"
|
||||||
v-for="color in themeColors"
|
:class="{ active: themeColor === color }" :style="{ backgroundColor: color }"
|
||||||
:key="color"
|
@click="changeThemeColor(color)">
|
||||||
class="color-item"
|
|
||||||
:class="{ active: themeColor === color }"
|
|
||||||
:style="{ backgroundColor: color }"
|
|
||||||
@click="changeThemeColor(color)"
|
|
||||||
>
|
|
||||||
<CheckOutlined v-if="themeColor === color" />
|
<CheckOutlined v-if="themeColor === color" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,14 +60,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineExpose, defineOptions, watch, onMounted } from 'vue'
|
import { ref, watch, onMounted } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { useLayoutStore } from '@/stores/modules/layout'
|
import { useLayoutStore } from '@/stores/modules/layout'
|
||||||
import { CheckOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
import { CheckOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
// 定义组件名称(多词命名)
|
// 定义组件名称(多词命名)
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'LayoutSetting'
|
name: 'LayoutSetting',
|
||||||
})
|
})
|
||||||
|
|
||||||
const layoutStore = useLayoutStore()
|
const layoutStore = useLayoutStore()
|
||||||
@@ -89,19 +80,10 @@ const showBreadcrumb = ref(true)
|
|||||||
const layoutModes = [
|
const layoutModes = [
|
||||||
{ value: 'default', label: '默认布局' },
|
{ value: 'default', label: '默认布局' },
|
||||||
{ value: 'menu', label: '菜单布局' },
|
{ value: 'menu', label: '菜单布局' },
|
||||||
{ value: 'top', label: '顶部布局' }
|
{ value: 'top', label: '顶部布局' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const themeColors = [
|
const themeColors = ['#1890ff', '#f5222d', '#fa541c', '#faad14', '#13c2c2', '#52c41a', '#2f54eb', '#722ed1']
|
||||||
'#1890ff',
|
|
||||||
'#f5222d',
|
|
||||||
'#fa541c',
|
|
||||||
'#faad14',
|
|
||||||
'#13c2c2',
|
|
||||||
'#52c41a',
|
|
||||||
'#2f54eb',
|
|
||||||
'#722ed1'
|
|
||||||
]
|
|
||||||
|
|
||||||
const openDrawer = () => {
|
const openDrawer = () => {
|
||||||
open.value = true
|
open.value = true
|
||||||
@@ -113,13 +95,13 @@ const closeDrawer = () => {
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openDrawer,
|
openDrawer,
|
||||||
closeDrawer
|
closeDrawer,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 切换布局
|
// 切换布局
|
||||||
const handleLayoutChange = (mode) => {
|
const handleLayoutChange = (mode) => {
|
||||||
layoutStore.setLayoutMode(mode)
|
layoutStore.setLayoutMode(mode)
|
||||||
const modeLabel = layoutModes.find(m => m.value === mode)?.label || mode
|
const modeLabel = layoutModes.find((m) => m.value === mode)?.label || mode
|
||||||
message.success(`已切换到${modeLabel}`)
|
message.success(`已切换到${modeLabel}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,19 +53,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted, defineOptions } from 'vue'
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useLayoutStore } from '@/stores/modules/layout'
|
import { useLayoutStore } from '@/stores/modules/layout'
|
||||||
import {
|
import { ReloadOutlined, CloseOutlined, ColumnWidthOutlined, CloseCircleOutlined } from '@ant-design/icons-vue'
|
||||||
ReloadOutlined,
|
import config from '@/config'
|
||||||
CloseOutlined,
|
|
||||||
ColumnWidthOutlined,
|
|
||||||
CloseCircleOutlined
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
|
|
||||||
// 定义组件名称(多词命名)
|
// 定义组件名称(多词命名)
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'TagsView'
|
name: 'TagsView',
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -91,7 +87,7 @@ const addTags = () => {
|
|||||||
name: name,
|
name: name,
|
||||||
query: route.query,
|
query: route.query,
|
||||||
params: route.params,
|
params: route.params,
|
||||||
meta: route.meta
|
meta: route.meta,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,12 +103,12 @@ const closeSelectedTag = (view) => {
|
|||||||
|
|
||||||
// 如果关闭的是当前激活的标签,需要跳转
|
// 如果关闭的是当前激活的标签,需要跳转
|
||||||
if (isActive(view)) {
|
if (isActive(view)) {
|
||||||
const nextTag = visitedViews.value.find(tag => tag.fullPath !== view.fullPath)
|
const nextTag = visitedViews.value.find((tag) => tag.fullPath !== view.fullPath)
|
||||||
if (nextTag) {
|
if (nextTag) {
|
||||||
router.push(nextTag.fullPath)
|
router.push(nextTag.fullPath)
|
||||||
} else {
|
} else {
|
||||||
// 如果没有其他标签,跳转到首页
|
// 如果没有其他标签,跳转到首页
|
||||||
router.push('/home')
|
router.push(config.DASHBOARD_URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,9 +120,7 @@ const closeOthersTags = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 保留固定标签和当前选中的标签
|
// 保留固定标签和当前选中的标签
|
||||||
const tagsToKeep = visitedViews.value.filter(tag =>
|
const tagsToKeep = visitedViews.value.filter((tag) => tag.meta?.affix || tag.fullPath === selectedTag.value.fullPath)
|
||||||
tag.meta?.affix || tag.fullPath === selectedTag.value.fullPath
|
|
||||||
)
|
|
||||||
|
|
||||||
// 更新标签列表
|
// 更新标签列表
|
||||||
layoutStore.viewTags = tagsToKeep
|
layoutStore.viewTags = tagsToKeep
|
||||||
@@ -140,7 +134,7 @@ const closeOthersTags = () => {
|
|||||||
// 关闭所有标签
|
// 关闭所有标签
|
||||||
const closeAllTags = () => {
|
const closeAllTags = () => {
|
||||||
// 只保留固定标签
|
// 只保留固定标签
|
||||||
const affixTags = visitedViews.value.filter(tag => tag.meta?.affix)
|
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix)
|
||||||
layoutStore.viewTags = affixTags
|
layoutStore.viewTags = affixTags
|
||||||
|
|
||||||
// 如果还有固定标签,跳转到第一个固定标签
|
// 如果还有固定标签,跳转到第一个固定标签
|
||||||
@@ -192,16 +186,20 @@ const handleMenuClick = ({ key }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 监听路由变化,自动添加标签
|
// 监听路由变化,自动添加标签
|
||||||
watch(() => route.fullPath, () => {
|
watch(
|
||||||
addTags()
|
() => route.fullPath,
|
||||||
// 更新当前选中的标签
|
() => {
|
||||||
selectedTag.value = visitedViews.value.find(tag => isActive(tag)) || {}
|
addTags()
|
||||||
}, { immediate: true })
|
// 更新当前选中的标签
|
||||||
|
selectedTag.value = visitedViews.value.find((tag) => isActive(tag)) || {}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
addTags()
|
addTags()
|
||||||
// 初始化选中的标签
|
// 初始化选中的标签
|
||||||
selectedTag.value = visitedViews.value.find(tag => isActive(tag)) || {}
|
selectedTag.value = visitedViews.value.find((tag) => isActive(tag)) || {}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -111,28 +111,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted, defineOptions } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
import { useUserStore } from '@/stores/modules/user'
|
import { useUserStore } from '@/stores/modules/user'
|
||||||
import { useI18nStore } from '@/stores/modules/i18n'
|
import { useI18nStore } from '@/stores/modules/i18n'
|
||||||
import {
|
import { DownOutlined, UserOutlined, LogoutOutlined, FullscreenOutlined, FullscreenExitOutlined, BellOutlined, CheckSquareOutlined, GlobalOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons-vue'
|
||||||
DownOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
LogoutOutlined,
|
|
||||||
FullscreenOutlined,
|
|
||||||
FullscreenExitOutlined,
|
|
||||||
BellOutlined,
|
|
||||||
CheckSquareOutlined,
|
|
||||||
GlobalOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
SettingOutlined
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
// 定义组件名称(多词命名)
|
// 定义组件名称(多词命名)
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'UserBar'
|
name: 'UserBar',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -147,7 +136,7 @@ const messages = ref([
|
|||||||
{ id: 1, title: '系统通知:新版本已发布', time: '10分钟前', read: false },
|
{ id: 1, title: '系统通知:新版本已发布', time: '10分钟前', read: false },
|
||||||
{ id: 2, title: '任务提醒:请完成待审核的用户', time: '30分钟前', read: false },
|
{ id: 2, title: '任务提醒:请完成待审核的用户', time: '30分钟前', read: false },
|
||||||
{ id: 3, title: '安全警告:检测到异常登录', time: '1小时前', read: true },
|
{ id: 3, title: '安全警告:检测到异常登录', time: '1小时前', read: true },
|
||||||
{ id: 4, title: '数据备份已完成', time: '2小时前', read: true }
|
{ id: 4, title: '数据备份已完成', time: '2小时前', read: true },
|
||||||
])
|
])
|
||||||
|
|
||||||
const messageCount = computed(() => messages.value.filter((m) => !m.read).length)
|
const messageCount = computed(() => messages.value.filter((m) => !m.read).length)
|
||||||
@@ -156,7 +145,7 @@ const messageCount = computed(() => messages.value.filter((m) => !m.read).length
|
|||||||
const tasks = ref([
|
const tasks = ref([
|
||||||
{ id: 1, title: '完成用户审核', completed: false },
|
{ id: 1, title: '完成用户审核', completed: false },
|
||||||
{ id: 2, title: '更新系统文档', completed: false },
|
{ id: 2, title: '更新系统文档', completed: false },
|
||||||
{ id: 3, title: '优化数据库查询', completed: true }
|
{ id: 3, title: '优化数据库查询', completed: true },
|
||||||
])
|
])
|
||||||
|
|
||||||
const taskCount = computed(() => tasks.value.filter((t) => !t.completed).length)
|
const taskCount = computed(() => tasks.value.filter((t) => !t.completed).length)
|
||||||
@@ -244,7 +233,7 @@ const handleLogout = () => {
|
|||||||
} catch {
|
} catch {
|
||||||
message.error(t('common.logoutFailed'))
|
message.error(t('common.logoutFailed'))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -89,7 +89,8 @@
|
|||||||
<div class="logo-box-top">
|
<div class="logo-box-top">
|
||||||
<span class="logo-text">VUE ADMIN</span>
|
<span class="logo-text">VUE ADMIN</span>
|
||||||
</div>
|
</div>
|
||||||
<a-menu v-model:selectedKeys="selectedKeys" mode="horizontal" :selected-keys="[route.path]" style="line-height: 60px">
|
<a-menu v-model:selectedKeys="selectedKeys" mode="horizontal" :selected-keys="[route.path]"
|
||||||
|
style="line-height: 60px">
|
||||||
<navMenu :menu-items="menuList" :active-path="route.path" />
|
<navMenu :menu-items="menuList" :active-path="route.path" />
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,7 +119,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineOptions, ref, watch, onMounted } from 'vue'
|
import { computed, ref, watch, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useLayoutStore } from '@/stores/modules/layout'
|
import { useLayoutStore } from '@/stores/modules/layout'
|
||||||
import { useUserStore } from '@/stores/modules/user'
|
import { useUserStore } from '@/stores/modules/user'
|
||||||
@@ -133,8 +134,8 @@ import setting from './components/setting.vue'
|
|||||||
|
|
||||||
// 定义组件名称(多词命名)
|
// 定义组件名称(多词命名)
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'AppLayouts'
|
name: 'AppLayouts',
|
||||||
});
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -149,9 +150,7 @@ const selectedParentMenu = computed(() => layoutStore.selectedParentMenu)
|
|||||||
|
|
||||||
// 缓存的视图列表
|
// 缓存的视图列表
|
||||||
const cachedViews = computed(() => {
|
const cachedViews = computed(() => {
|
||||||
return layoutStore.viewTags
|
return layoutStore.viewTags.filter((tag) => !tag.meta?.noCache).map((tag) => tag.name)
|
||||||
.filter(tag => !tag.meta?.noCache)
|
|
||||||
.map(tag => tag.name)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 布局类名
|
// 布局类名
|
||||||
@@ -160,7 +159,7 @@ const layoutClass = computed(() => {
|
|||||||
'layout-default': layoutMode.value === 'default',
|
'layout-default': layoutMode.value === 'default',
|
||||||
'layout-menu': layoutMode.value === 'menu',
|
'layout-menu': layoutMode.value === 'menu',
|
||||||
'layout-top': layoutMode.value === 'top',
|
'layout-top': layoutMode.value === 'top',
|
||||||
'is-collapse': sidebarCollapsed.value
|
'is-collapse': sidebarCollapsed.value,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -207,8 +206,8 @@ const updateMenuState = () => {
|
|||||||
selectedKeys.value = [route.path]
|
selectedKeys.value = [route.path]
|
||||||
|
|
||||||
// 获取所有父级路径
|
// 获取所有父级路径
|
||||||
const matched = route.matched.filter(item => item.path !== '/' && item.path !== route.path)
|
const matched = route.matched.filter((item) => item.path !== '/' && item.path !== route.path)
|
||||||
const parentPaths = matched.map(item => item.path)
|
const parentPaths = matched.map((item) => item.path)
|
||||||
|
|
||||||
// 对于不同的布局模式,处理方式不同
|
// 对于不同的布局模式,处理方式不同
|
||||||
if (layoutMode.value === 'default') {
|
if (layoutMode.value === 'default') {
|
||||||
@@ -271,24 +270,34 @@ const findParentMenu = (menus, path) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 监听路由变化,更新菜单状态
|
// 监听路由变化,更新菜单状态
|
||||||
watch(() => route.path, (newPath) => {
|
watch(
|
||||||
console.log('路由变化:', newPath)
|
() => route.path,
|
||||||
updateMenuState()
|
(newPath) => {
|
||||||
}, { immediate: true })
|
console.log('路由变化:', newPath)
|
||||||
|
updateMenuState()
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
// 监听布局模式变化,确保菜单状态正确
|
// 监听布局模式变化,确保菜单状态正确
|
||||||
watch(() => layoutMode.value, () => {
|
watch(
|
||||||
updateMenuState()
|
() => layoutMode.value,
|
||||||
})
|
() => {
|
||||||
|
updateMenuState()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// 监听折叠状态
|
// 监听折叠状态
|
||||||
watch(() => sidebarCollapsed.value, (val) => {
|
watch(
|
||||||
if (val) {
|
() => sidebarCollapsed.value,
|
||||||
openKeys.value = []
|
(val) => {
|
||||||
} else {
|
if (val) {
|
||||||
updateMenuState()
|
openKeys.value = []
|
||||||
}
|
} else {
|
||||||
})
|
updateMenuState()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -9,41 +9,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="search-form">
|
<div class="search-form">
|
||||||
<a-form :model="searchForm" layout="inline">
|
<sc-form :form-items="formItems" :initial-values="searchForm" :show-actions="true" submit-text="查询"
|
||||||
<a-form-item label="用户名">
|
reset-text="重置" @finish="handleSearch" @reset="handleReset" layout="inline" />
|
||||||
<a-input v-model:value="searchForm.username" placeholder="请输入用户名" />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="状态">
|
|
||||||
<a-select v-model:value="searchForm.status" placeholder="请选择状态" style="width: 120px">
|
|
||||||
<a-select-option value="">全部</a-select-option>
|
|
||||||
<a-select-option :value="1">正常</a-select-option>
|
|
||||||
<a-select-option :value="0">禁用</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<a-space>
|
|
||||||
<a-button type="primary" @click="handleSearch">查询</a-button>
|
|
||||||
<a-button @click="handleReset">重置</a-button>
|
|
||||||
</a-space>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination">
|
<sc-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination"
|
||||||
<template #bodyCell="{ column, record }">
|
:show-action-column="true" :action-column="{
|
||||||
<template v-if="column.key === 'status'">
|
title: '操作',
|
||||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
key: 'action',
|
||||||
{{ record.status === 1 ? '正常' : '禁用' }}
|
width: 150,
|
||||||
</a-tag>
|
}">
|
||||||
</template>
|
<template #status="{ record }">
|
||||||
<template v-else-if="column.key === 'action'">
|
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||||
<a-space>
|
{{ record.status === 1 ? '正常' : '禁用' }}
|
||||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
</a-tag>
|
||||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
|
||||||
</a-space>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
<template #action="{ record }">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||||
|
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</sc-table>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -52,45 +39,66 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||||
|
import ScTable from '@/components/scTable/index.vue'
|
||||||
|
import ScForm from '@/components/scForm/index.vue'
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
dataIndex: 'id',
|
dataIndex: 'id',
|
||||||
key: 'id',
|
key: 'id',
|
||||||
width: 80
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '用户名',
|
title: '用户名',
|
||||||
dataIndex: 'username',
|
dataIndex: 'username',
|
||||||
key: 'username'
|
key: 'username',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '昵称',
|
title: '昵称',
|
||||||
dataIndex: 'nickname',
|
dataIndex: 'nickname',
|
||||||
key: 'nickname'
|
key: 'nickname',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
width: 100
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
dataIndex: 'createTime',
|
dataIndex: 'createTime',
|
||||||
key: 'createTime'
|
key: 'createTime',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 搜索表单配置
|
||||||
|
const formItems = [
|
||||||
|
{
|
||||||
|
field: 'username',
|
||||||
|
label: '用户名',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入用户名',
|
||||||
|
allowClear: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
field: 'status',
|
||||||
key: 'action',
|
label: '状态',
|
||||||
width: 150
|
type: 'select',
|
||||||
}
|
placeholder: '请选择状态',
|
||||||
|
options: [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '正常', value: 1 },
|
||||||
|
{ label: '禁用', value: 0 },
|
||||||
|
],
|
||||||
|
allowClear: true,
|
||||||
|
style: 'width: 120px',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const searchForm = ref({
|
const searchForm = ref({
|
||||||
username: '',
|
username: '',
|
||||||
status: ''
|
status: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const dataSource = ref([])
|
const dataSource = ref([])
|
||||||
@@ -100,7 +108,7 @@ const pagination = ref({
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
showTotal: (total) => `共 ${total} 条`
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 模拟数据
|
// 模拟数据
|
||||||
@@ -110,15 +118,15 @@ const mockData = [
|
|||||||
username: 'admin',
|
username: 'admin',
|
||||||
nickname: '管理员',
|
nickname: '管理员',
|
||||||
status: 1,
|
status: 1,
|
||||||
createTime: '2024-01-01 10:00:00'
|
createTime: '2024-01-01 10:00:00',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
username: 'user',
|
username: 'user',
|
||||||
nickname: '普通用户',
|
nickname: '普通用户',
|
||||||
status: 1,
|
status: 1,
|
||||||
createTime: '2024-01-02 10:00:00'
|
createTime: '2024-01-02 10:00:00',
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const loadData = () => {
|
const loadData = () => {
|
||||||
@@ -138,7 +146,7 @@ const handleSearch = () => {
|
|||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
searchForm.value = {
|
searchForm.value = {
|
||||||
username: '',
|
username: '',
|
||||||
status: ''
|
status: '',
|
||||||
}
|
}
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
105
src/pages/ucenter/components/BasicInfo.vue
Normal file
105
src/pages/ucenter/components/BasicInfo.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<scForm :form-items="formItems" :initial-values="initialValues" :loading="loading" @finish="handleFinish"
|
||||||
|
@reset="handleReset" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import scForm from '@/components/scForm/index.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
userInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update'])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 表单初始值
|
||||||
|
const initialValues = computed(() => ({
|
||||||
|
username: props.userInfo.username || '',
|
||||||
|
nickname: props.userInfo.nickname || '',
|
||||||
|
phone: props.userInfo.phone || '',
|
||||||
|
email: props.userInfo.email || '',
|
||||||
|
gender: props.userInfo.gender || 0,
|
||||||
|
birthday: props.userInfo.birthday || null,
|
||||||
|
bio: props.userInfo.bio || '',
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 表单项配置
|
||||||
|
const formItems = [
|
||||||
|
{
|
||||||
|
field: 'username',
|
||||||
|
label: '用户名',
|
||||||
|
type: 'input',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'nickname',
|
||||||
|
label: '昵称',
|
||||||
|
type: 'input',
|
||||||
|
required: true,
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请输入昵称', trigger: 'blur' },
|
||||||
|
{ min: 2, max: 20, message: '昵称长度在 2 到 20 个字符', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'phone',
|
||||||
|
label: '手机号',
|
||||||
|
type: 'input',
|
||||||
|
rules: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'email',
|
||||||
|
label: '邮箱',
|
||||||
|
type: 'input',
|
||||||
|
rules: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'gender',
|
||||||
|
label: '性别',
|
||||||
|
type: 'radio',
|
||||||
|
options: [
|
||||||
|
{ label: '男', value: 1 },
|
||||||
|
{ label: '女', value: 2 },
|
||||||
|
{ label: '保密', value: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'birthday',
|
||||||
|
label: '生日',
|
||||||
|
type: 'date',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'bio',
|
||||||
|
label: '个人简介',
|
||||||
|
type: 'textarea',
|
||||||
|
rows: 4,
|
||||||
|
maxLength: 200,
|
||||||
|
showCount: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 表单提交
|
||||||
|
const handleFinish = (values) => {
|
||||||
|
loading.value = true
|
||||||
|
// 模拟接口请求
|
||||||
|
setTimeout(() => {
|
||||||
|
emit('update', values)
|
||||||
|
message.success('保存成功')
|
||||||
|
loading.value = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const handleReset = () => {
|
||||||
|
message.info('已重置')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
81
src/pages/ucenter/components/Password.vue
Normal file
81
src/pages/ucenter/components/Password.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<scForm :form-items="formItems" :initial-values="initialValues" :loading="loading" submit-text="修改密码"
|
||||||
|
@finish="handleFinish" @reset="handleReset" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import scForm from '@/components/scForm/index.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['success'])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 表单初始值
|
||||||
|
const initialValues = {
|
||||||
|
oldPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单项配置
|
||||||
|
const formItems = [
|
||||||
|
{
|
||||||
|
field: 'oldPassword',
|
||||||
|
label: '原密码',
|
||||||
|
type: 'password',
|
||||||
|
required: true,
|
||||||
|
rules: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'newPassword',
|
||||||
|
label: '新密码',
|
||||||
|
type: 'password',
|
||||||
|
required: true,
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'confirmPassword',
|
||||||
|
label: '确认密码',
|
||||||
|
type: 'password',
|
||||||
|
required: true,
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (rule, value) => {
|
||||||
|
if (value !== initialValues.newPassword) {
|
||||||
|
return Promise.reject('两次输入的密码不一致')
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 表单提交
|
||||||
|
const handleFinish = (values) => {
|
||||||
|
loading.value = true
|
||||||
|
// 模拟接口请求
|
||||||
|
setTimeout(() => {
|
||||||
|
message.success('密码修改成功,请重新登录')
|
||||||
|
emit('success')
|
||||||
|
handleReset()
|
||||||
|
loading.value = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const handleReset = () => {
|
||||||
|
initialValues.oldPassword = ''
|
||||||
|
initialValues.newPassword = ''
|
||||||
|
initialValues.confirmPassword = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
73
src/pages/ucenter/components/ProfileInfo.vue
Normal file
73
src/pages/ucenter/components/ProfileInfo.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div class="profile-info">
|
||||||
|
<div class="avatar-wrapper">
|
||||||
|
<a-avatar :size="100" :src="userInfo.avatar" @click="handleAvatarClick">
|
||||||
|
{{ userInfo.nickname?.charAt(0) }}
|
||||||
|
</a-avatar>
|
||||||
|
</div>
|
||||||
|
<div class="user-name">{{ userInfo.nickname || userInfo.username }}</div>
|
||||||
|
<a-tag :color="userInfo.status === 1 ? 'green' : 'red'">
|
||||||
|
{{ userInfo.status === 1 ? '正常' : '禁用' }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
userInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['avatar-click'])
|
||||||
|
|
||||||
|
const handleAvatarClick = () => {
|
||||||
|
emit('avatar-click')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.profile-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.ant-avatar {
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.6);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tag {
|
||||||
|
margin: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
85
src/pages/ucenter/components/Security.vue
Normal file
85
src/pages/ucenter/components/Security.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<a-list :data-source="securityList" item-layout="horizontal">
|
||||||
|
<template #renderItem="{ item }">
|
||||||
|
<a-list-item>
|
||||||
|
<a-list-item-meta>
|
||||||
|
<template #title>
|
||||||
|
{{ item.title }}
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
{{ item.description }}
|
||||||
|
</template>
|
||||||
|
</a-list-item-meta>
|
||||||
|
<template #actions>
|
||||||
|
<a-button type="primary" size="small" @click="handleAction(item.action)">
|
||||||
|
{{ item.buttonText }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</a-list-item>
|
||||||
|
</template>
|
||||||
|
</a-list>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['change-password'])
|
||||||
|
|
||||||
|
const securityList = ref([
|
||||||
|
{
|
||||||
|
title: '登录密码',
|
||||||
|
description: '用于登录系统的密码,建议定期更换',
|
||||||
|
buttonText: '修改',
|
||||||
|
action: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '手机验证',
|
||||||
|
description: '用于接收重要通知和安全验证',
|
||||||
|
buttonText: '已绑定',
|
||||||
|
action: 'phone',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '邮箱验证',
|
||||||
|
description: '用于接收重要通知和账号找回',
|
||||||
|
buttonText: '已绑定',
|
||||||
|
action: 'email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '登录设备',
|
||||||
|
description: '查看和管理已登录的设备',
|
||||||
|
buttonText: '查看',
|
||||||
|
action: 'device',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleAction = (action) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'password':
|
||||||
|
emit('change-password')
|
||||||
|
break
|
||||||
|
case 'phone':
|
||||||
|
message.info('手机绑定功能开发中')
|
||||||
|
break
|
||||||
|
case 'email':
|
||||||
|
message.info('邮箱绑定功能开发中')
|
||||||
|
break
|
||||||
|
case 'device':
|
||||||
|
message.info('登录设备管理功能开发中')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.ant-list-item) {
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-list-item:last-child) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
202
src/pages/ucenter/index.vue
Normal file
202
src/pages/ucenter/index.vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ucenter">
|
||||||
|
<a-card>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="6">
|
||||||
|
<ProfileInfo :user-info="userInfo" @avatar-click="showAvatarModal = true" />
|
||||||
|
<a-menu v-model:selectedKeys="selectedKeys" mode="inline" class="menu">
|
||||||
|
<a-menu-item key="basic">
|
||||||
|
<UserOutlined />
|
||||||
|
基本信息
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="password">
|
||||||
|
<LockOutlined />
|
||||||
|
修改密码
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="security">
|
||||||
|
<SafetyOutlined />
|
||||||
|
账号安全
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="18">
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<BasicInfo v-if="selectedKeys[0] === 'basic'" :user-info="userInfo"
|
||||||
|
@update="handleUpdateUserInfo" />
|
||||||
|
<Password v-else-if="selectedKeys[0] === 'password'" @success="handlePasswordSuccess" />
|
||||||
|
<Security v-else-if="selectedKeys[0] === 'security'" @change-password="handleChangePassword" />
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 头像上传弹窗 -->
|
||||||
|
<a-modal v-model:open="showAvatarModal" title="更换头像" :confirm-loading="loading" @ok="handleAvatarUpload"
|
||||||
|
@cancel="showAvatarModal = false">
|
||||||
|
<div class="avatar-upload">
|
||||||
|
<a-upload list-type="picture-card" :max-count="1" :before-upload="beforeUpload"
|
||||||
|
@change="handleAvatarChange" :file-list="avatarFileList">
|
||||||
|
<div v-if="avatarFileList.length === 0">
|
||||||
|
<PlusOutlined />
|
||||||
|
<div class="ant-upload-text">上传头像</div>
|
||||||
|
</div>
|
||||||
|
</a-upload>
|
||||||
|
<div class="upload-tip">
|
||||||
|
<a-typography-text type="secondary"> 支持 JPG、PNG 格式,文件大小不超过 2MB </a-typography-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { PlusOutlined, UserOutlined, LockOutlined, SafetyOutlined } from '@ant-design/icons-vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import ProfileInfo from './components/ProfileInfo.vue'
|
||||||
|
import BasicInfo from './components/BasicInfo.vue'
|
||||||
|
import Password from './components/Password.vue'
|
||||||
|
import Security from './components/Security.vue'
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
const userInfo = ref({
|
||||||
|
username: '',
|
||||||
|
nickname: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
avatar: '',
|
||||||
|
status: 1,
|
||||||
|
gender: 0,
|
||||||
|
birthday: null,
|
||||||
|
bio: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选中的菜单
|
||||||
|
const selectedKeys = ref(['basic'])
|
||||||
|
|
||||||
|
// 头像上传
|
||||||
|
const showAvatarModal = ref(false)
|
||||||
|
const avatarFileList = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 初始化用户信息
|
||||||
|
const initUserInfo = () => {
|
||||||
|
// 模拟用户数据
|
||||||
|
const mockUserInfo = {
|
||||||
|
username: 'admin',
|
||||||
|
nickname: '管理员',
|
||||||
|
phone: '13800138000',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
avatar: '',
|
||||||
|
status: 1,
|
||||||
|
gender: 1,
|
||||||
|
birthday: dayjs('1990-01-01'),
|
||||||
|
bio: '热爱编程,专注于前端开发技术。',
|
||||||
|
}
|
||||||
|
userInfo.value = { ...mockUserInfo }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
const handleUpdateUserInfo = (data) => {
|
||||||
|
Object.assign(userInfo.value, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码修改成功
|
||||||
|
const handlePasswordSuccess = () => {
|
||||||
|
// 密码修改成功后的处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换到密码修改页面
|
||||||
|
const handleChangePassword = () => {
|
||||||
|
selectedKeys.value = ['password']
|
||||||
|
}
|
||||||
|
|
||||||
|
// 头像上传前校验
|
||||||
|
const beforeUpload = (file) => {
|
||||||
|
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
|
||||||
|
if (!isJpgOrPng) {
|
||||||
|
message.error('只能上传 JPG/PNG 格式的文件!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const isLt2M = file.size / 1024 / 1024 < 2
|
||||||
|
if (!isLt2M) {
|
||||||
|
message.error('图片大小不能超过 2MB!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false // 阻止自动上传
|
||||||
|
}
|
||||||
|
|
||||||
|
// 头像文件变化
|
||||||
|
const handleAvatarChange = ({ fileList }) => {
|
||||||
|
avatarFileList.value = fileList
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传头像
|
||||||
|
const handleAvatarUpload = () => {
|
||||||
|
if (avatarFileList.value.length === 0) {
|
||||||
|
message.warning('请先选择头像')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
// 模拟上传
|
||||||
|
setTimeout(() => {
|
||||||
|
const file = avatarFileList.value[0]
|
||||||
|
userInfo.value.avatar = URL.createObjectURL(file.originFileObj)
|
||||||
|
message.success('头像更新成功')
|
||||||
|
showAvatarModal.value = false
|
||||||
|
avatarFileList.value = []
|
||||||
|
loading.value = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initUserInfo()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.ucenter {
|
||||||
|
.content-wrapper {
|
||||||
|
padding: 24px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
margin-top: 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.ant-menu-item {
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 4px 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-menu-item-selected {
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border-right-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-upload {
|
||||||
|
.upload-tip {
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-head-title) {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import config from '@/config'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础路由(不需要登录)
|
* 基础路由(不需要登录)
|
||||||
*/
|
*/
|
||||||
@@ -8,8 +10,8 @@ const systemRoutes = [
|
|||||||
component: () => import('../pages/login/index.vue'),
|
component: () => import('../pages/login/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'login',
|
title: 'login',
|
||||||
hidden: true
|
hidden: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/register',
|
path: '/register',
|
||||||
@@ -17,8 +19,8 @@ const systemRoutes = [
|
|||||||
component: () => import('../pages/login/userRegister.vue'),
|
component: () => import('../pages/login/userRegister.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'register',
|
title: 'register',
|
||||||
hidden: true
|
hidden: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/reset-password',
|
path: '/reset-password',
|
||||||
@@ -26,16 +28,16 @@ const systemRoutes = [
|
|||||||
component: () => import('../pages/login/resetPassword.vue'),
|
component: () => import('../pages/login/resetPassword.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'resetPassword',
|
title: 'resetPassword',
|
||||||
hidden: true
|
hidden: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Layout',
|
name: 'Layout',
|
||||||
component: () => import('@/layouts/index.vue'),
|
component: () => import('@/layouts/index.vue'),
|
||||||
redirect: '/home',
|
redirect: config.DASHBOARD_URL,
|
||||||
children: []
|
children: [],
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default systemRoutes
|
export default systemRoutes
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ export const useLayoutStore = defineStore(
|
|||||||
// 侧边栏折叠状态
|
// 侧边栏折叠状态
|
||||||
const sidebarCollapsed = ref(false)
|
const sidebarCollapsed = ref(false)
|
||||||
|
|
||||||
|
// 主题颜色
|
||||||
|
const themeColor = ref('#1890ff')
|
||||||
|
|
||||||
|
// 显示标签栏
|
||||||
|
const showTags = ref(true)
|
||||||
|
|
||||||
|
// 显示面包屑
|
||||||
|
const showBreadcrumb = ref(true)
|
||||||
|
|
||||||
// 当前选中的父菜单(用于双栏布局)
|
// 当前选中的父菜单(用于双栏布局)
|
||||||
const selectedParentMenu = ref(null)
|
const selectedParentMenu = ref(null)
|
||||||
|
|
||||||
@@ -55,24 +64,57 @@ export const useLayoutStore = defineStore(
|
|||||||
viewTags.value = []
|
viewTags.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置主题颜色
|
||||||
|
const setThemeColor = (color) => {
|
||||||
|
themeColor.value = color
|
||||||
|
document.documentElement.style.setProperty('--primary-color', color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置标签栏显示
|
||||||
|
const setShowTags = (show) => {
|
||||||
|
showTags.value = show
|
||||||
|
document.documentElement.style.setProperty('--show-tags', show ? 'block' : 'none')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置面包屑显示
|
||||||
|
const setShowBreadcrumb = (show) => {
|
||||||
|
showBreadcrumb.value = show
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置主题设置
|
||||||
|
const resetTheme = () => {
|
||||||
|
themeColor.value = '#1890ff'
|
||||||
|
showTags.value = true
|
||||||
|
showBreadcrumb.value = true
|
||||||
|
document.documentElement.style.setProperty('--primary-color', '#1890ff')
|
||||||
|
document.documentElement.style.setProperty('--show-tags', 'block')
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
layoutMode,
|
layoutMode,
|
||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
selectedParentMenu,
|
selectedParentMenu,
|
||||||
viewTags,
|
viewTags,
|
||||||
|
themeColor,
|
||||||
|
showTags,
|
||||||
|
showBreadcrumb,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
setLayoutMode,
|
setLayoutMode,
|
||||||
setSelectedParentMenu,
|
setSelectedParentMenu,
|
||||||
updateViewTags,
|
updateViewTags,
|
||||||
removeViewTags,
|
removeViewTags,
|
||||||
clearViewTags,
|
clearViewTags,
|
||||||
|
setThemeColor,
|
||||||
|
setShowTags,
|
||||||
|
setShowBreadcrumb,
|
||||||
|
resetTheme,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
persist: {
|
persist: {
|
||||||
key: 'layout-store',
|
key: 'layout-store',
|
||||||
storage: customStorage,
|
storage: customStorage,
|
||||||
pick: ['layoutMode', 'sidebarCollapsed']
|
pick: ['layoutMode', 'sidebarCollapsed', 'themeColor', 'showTags', 'showBreadcrumb'],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user