|
|
@@ -1,237 +1,1483 @@
|
|
|
-<template>
|
|
|
- <div class="app-container">
|
|
|
- <div class="hearder">
|
|
|
- <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="68px">
|
|
|
- <el-form-item label="配置名称" prop="mapName">
|
|
|
- <el-input v-model="queryParams.paramType" placeholder="请输入配置名称" clearable />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item>
|
|
|
- <el-button type="primary" icon="el-icon-search" size="mini" @click="">搜索</el-button>
|
|
|
- <el-button icon="el-icon-refresh" size="mini" @click="">重置</el-button>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- </div>
|
|
|
- <el-row :gutter="10" class="mb8">
|
|
|
- <el-col :span="1.5">
|
|
|
- <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="addData">新建配置</el-button>
|
|
|
- </el-col>
|
|
|
- <el-col :span="1.5">
|
|
|
- <el-button type="primary" plain icon="el-icon-edit" size="mini" @click="editData"
|
|
|
- :disabled="single">修改配置</el-button>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
- <div class="individ-tables">
|
|
|
- <el-table :data="data" border style="width: 100%" @selection-change="handleSelectionChange">
|
|
|
- <el-table-column type="selection" width="45" align="center" />
|
|
|
- <el-table-column prop="id" label="编号" width="60" align="center"></el-table-column>
|
|
|
- <el-table-column prop="theme" label="个性名称" align="center"></el-table-column>
|
|
|
- <el-table-column prop="logo" label="顶部logo" align="center">
|
|
|
- <template slot-scope="scope">
|
|
|
- <el-image style="width: 50px; height: 30px" :src="scope.row.logo" fit="fill">
|
|
|
- </el-image>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="loginLogo" label="登录页logo" align="center">
|
|
|
- <template slot-scope="scope">
|
|
|
- <el-image style="width: 50px; height: 30px" :src="scope.row.loginLogo" fit="fill">
|
|
|
- </el-image>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="backgroundImg" label="登录页背景图片" align="center">
|
|
|
- <template slot-scope="scope">
|
|
|
- <el-image style="width: 50px; height: 30px" :src="scope.row.backgroundImg" fit="fill">
|
|
|
- </el-image>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="company" label="公司名称" align="center"></el-table-column>
|
|
|
- <el-table-column prop="status" label="当前状态" align="center"></el-table-column>
|
|
|
- <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
|
|
|
- <template slot-scope="scope">
|
|
|
- <el-button size="mini" type="text" icon="el-icon-edit-outline" @click="editData(scope.row)">编辑</el-button>
|
|
|
- <el-button size="mini" type="text" icon="el-icon-delete" @click="removeParams(scope.row)">删除</el-button>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
- </div>
|
|
|
- <div class="indiv-dia">
|
|
|
- <el-dialog :title="title" :visible.sync="dialogVisible" width="600px" @close="clearForm">
|
|
|
- <el-form ref="ruleForm" :model="form" :rules="rules" label-width="120px">
|
|
|
- <el-form-item label="个性名称" prop="theme">
|
|
|
- <el-input v-model="form.theme" placeholder="请输入内容" max="20"></el-input>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="顶部logo" prop="logo">
|
|
|
- <ImageUpload :isShowTip="false" :limit="1" @input="(fileList) => handleInput(fileList, 'logo')"
|
|
|
- :value="form.logo"></ImageUpload>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="登录页logo" prop="loginLogo">
|
|
|
- <ImageUpload :isShowTip="false" :limit="1" @input="(fileList) => handleInput(fileList, 'logoLogin')"
|
|
|
- :value="form.loginLogo"></ImageUpload>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="登录页背景图片" prop="backgroundImg">
|
|
|
- <ImageUpload :isShowTip="false" :limit="1" @input="(fileList) => handleInput(fileList, 'loginGround')"
|
|
|
- :value="form.backgroundImg"></ImageUpload>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="公司名称" prop="company">
|
|
|
- <el-input v-model="form.company" placeholder="请输入内容" max="20"></el-input>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="当前状态">
|
|
|
- <el-radio-group v-model="form.status">
|
|
|
- <el-radio v-for="dict in dict.type.sys_notice_status" :key="dict.value" :label="dict.value">{{ dict.label
|
|
|
- }}</el-radio>
|
|
|
- </el-radio-group>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- <span slot="footer" class="dialog-footer">
|
|
|
- <el-button @click="dialogVisible = false">取 消</el-button>
|
|
|
- <el-button type="primary" @click="submitData('ruleForm')">确 定</el-button>
|
|
|
- </span>
|
|
|
- </el-dialog>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script>
|
|
|
-export default {
|
|
|
- dicts: ['sys_notice_status'],
|
|
|
- data() {
|
|
|
- return {
|
|
|
- title: '',
|
|
|
- dialogVisible: false,
|
|
|
- ids: [],
|
|
|
- // 非单个禁用
|
|
|
- single: true,
|
|
|
- // 非多个禁用
|
|
|
- multiple: true,
|
|
|
- queryParams: {
|
|
|
- pageNum: 1,
|
|
|
- pageSize: 10,
|
|
|
- theme: '',
|
|
|
- logo: '',
|
|
|
- loginLogo: '',
|
|
|
- backgroundImg: '',
|
|
|
- company: '',
|
|
|
- status: ''
|
|
|
- },
|
|
|
- form: {
|
|
|
- id: '',
|
|
|
- theme: '',
|
|
|
- logo: '',
|
|
|
- loginLogo: '',
|
|
|
- backgroundImg: '',
|
|
|
- company: '',
|
|
|
- status: '1'
|
|
|
- },
|
|
|
- data: [
|
|
|
-
|
|
|
- ],
|
|
|
- rules: {
|
|
|
- theme: [
|
|
|
- { required: true, message: "请输入个性名称", trigger: "blur" }
|
|
|
- ],
|
|
|
- logo: [
|
|
|
- { required: true, message: "请上传顶部logo", trigger: "blur" }
|
|
|
- ],
|
|
|
- loginLogo: [
|
|
|
- { required: true, message: "请上传登录页logo", trigger: "blur" }
|
|
|
- ],
|
|
|
- backgroundImg: [
|
|
|
- { required: true, message: "请上传背景页图片", trigger: "blur" }
|
|
|
- ],
|
|
|
- company: [
|
|
|
- { required: true, message: "请输入公司名称", trigger: "blur" }
|
|
|
- ],
|
|
|
- status: [
|
|
|
- { required: true, message: "启用状态", trigger: "blur" }
|
|
|
- ]
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- methods: {
|
|
|
- handleSelectionChange(selection) {
|
|
|
- this.ids = selection.map(item => item.id)
|
|
|
- this.names = selection.map(item => item.mapName)
|
|
|
- this.single = selection.length !== 1
|
|
|
- this.multiple = !selection.length
|
|
|
- },
|
|
|
- addData() {
|
|
|
- this.title = "新增配置"
|
|
|
- this.dialogVisible = true;
|
|
|
- },
|
|
|
- editData(row) {
|
|
|
- this.title = "修改配置"
|
|
|
- let id = this.ids || row.id;
|
|
|
- this.form = this.data.find(item => item.id == id);
|
|
|
- this.dialogVisible = true;
|
|
|
- },
|
|
|
- clearForm() {
|
|
|
- this.form = {
|
|
|
- id: '',
|
|
|
- theme: '',
|
|
|
- logo: '',
|
|
|
- loginLogo: '',
|
|
|
- backgroundImg: '',
|
|
|
- company: '',
|
|
|
- status: '1'
|
|
|
- }
|
|
|
- },
|
|
|
- // 图片上传的回调
|
|
|
- handleInput(fileList, extraParam) {
|
|
|
- if (extraParam === 'logo') {
|
|
|
- if (this.form.logo) {
|
|
|
- this.form.logo = null;
|
|
|
- }
|
|
|
- this.form.logo = fileList;
|
|
|
- } else if (extraParam === 'logoLogin') {
|
|
|
- if (this.form.loginLogo) {
|
|
|
- this.form.loginLogo = null;
|
|
|
- }
|
|
|
- this.form.loginLogo = fileList;
|
|
|
- } else if (extraParam === 'loginGround') {
|
|
|
- if (this.form.backgroundImg) {
|
|
|
- this.form.backgroundImg = null;
|
|
|
- }
|
|
|
- this.form.backgroundImg = fileList;
|
|
|
- }
|
|
|
- },
|
|
|
- submitData(ruleForm) {
|
|
|
- this.$refs[ruleForm].validate((valid) => {
|
|
|
- if (valid) {
|
|
|
- this.dialogVisible = false;
|
|
|
- // todo 模拟id(开始)
|
|
|
- if(this.data.length > 0){
|
|
|
- let id = this.data[this.data.length -1].id;
|
|
|
- this.form.id = id + 1;
|
|
|
- } else {
|
|
|
- this.form.id = 1;
|
|
|
- }
|
|
|
- // todo 模拟id(结束)
|
|
|
- this.data.push(this.form)
|
|
|
- console.log(this.data);
|
|
|
- } else {
|
|
|
- return false;
|
|
|
- }
|
|
|
- })
|
|
|
- },
|
|
|
- removeParams(row){
|
|
|
- // 删除
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped>
|
|
|
-::v-deep .indiv-dia .el-dialog {
|
|
|
- margin-top: 3vh !important;
|
|
|
- border-radius: 10px;
|
|
|
-}
|
|
|
-
|
|
|
-::v-deep .indiv-dia .el-dialog:not(.is-fullscreen) {
|
|
|
- margin-top: 3vh !important;
|
|
|
- border-radius: 10px;
|
|
|
-}
|
|
|
-
|
|
|
-::v-deep .el-table .el-table__header-wrapper th,
|
|
|
-.el-table .el-table__fixed-header-wrapper th {
|
|
|
- background-color: #f6f6f6 !important;
|
|
|
-}
|
|
|
-</style>
|
|
|
+<template>
|
|
|
+ <div class="individuation-page">
|
|
|
+ <!-- 页面头部 -->
|
|
|
+ <div class="page-header">
|
|
|
+ <div class="header-content">
|
|
|
+ <div class="header-main">
|
|
|
+ <h1 class="page-title">个性配置</h1>
|
|
|
+ <p class="page-desc">配置主题颜色与登录资源,修改后即时生效</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 未保存更改提示 -->
|
|
|
+ <div v-if="hasUnsavedChanges" class="unsaved-warning">
|
|
|
+ <div class="warning-content">
|
|
|
+ <i class="el-icon-warning"></i>
|
|
|
+ <span>有未保存更改,请及时保存以免丢失</span>
|
|
|
+ <el-button type="text" size="mini" @click="saveConfig">立即保存</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 主内容区域 -->
|
|
|
+ <div class="page-content">
|
|
|
+ <div class="config-card">
|
|
|
+ <div class="card-header">
|
|
|
+ <h2 class="card-title">主题配置</h2>
|
|
|
+ <p class="card-desc">自定义主题颜色与登录资源,修改后即时生效</p>
|
|
|
+ </div>
|
|
|
+ <div class="card-content">
|
|
|
+ <el-form ref="configForm" :model="config" label-width="120px" class="config-form">
|
|
|
+ <!-- 主题主色 -->
|
|
|
+ <el-form-item label="主题主色" class="color-form-item">
|
|
|
+ <div class="color-input-group">
|
|
|
+ <el-input
|
|
|
+ v-model="primaryColorInput"
|
|
|
+ @input="onPrimaryColorInputChange"
|
|
|
+ placeholder="1677ff"
|
|
|
+ class="color-input"
|
|
|
+ :class="{ 'input-error': !isPrimaryColorValid }"
|
|
|
+ maxlength="6"
|
|
|
+ >
|
|
|
+ <template slot="prepend">#</template>
|
|
|
+ <template slot="append">
|
|
|
+ <el-color-picker
|
|
|
+ v-model="config.primaryColor"
|
|
|
+ @change="onPrimaryColorChange"
|
|
|
+ color-format="hex"
|
|
|
+ class="color-picker-append"
|
|
|
+ size="mini"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ </div>
|
|
|
+ <div class="form-hint">请输入 HEX 色值或通过取色器选择</div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 主题辅色 -->
|
|
|
+ <el-form-item label="主题辅色" class="color-form-item">
|
|
|
+ <div class="color-input-group">
|
|
|
+ <el-input
|
|
|
+ v-model="secondaryColorInput"
|
|
|
+ @input="onSecondaryColorInputChange"
|
|
|
+ placeholder="52c41a"
|
|
|
+ class="color-input"
|
|
|
+ :class="{ 'input-error': !isSecondaryColorValid }"
|
|
|
+ maxlength="6"
|
|
|
+ >
|
|
|
+ <template slot="prepend">#</template>
|
|
|
+ <template slot="append">
|
|
|
+ <el-color-picker
|
|
|
+ v-model="config.secondaryColor"
|
|
|
+ @change="onSecondaryColorChange"
|
|
|
+ color-format="hex"
|
|
|
+ class="color-picker-append"
|
|
|
+ size="mini"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ </div>
|
|
|
+ <div class="form-hint">请输入 HEX 色值或通过取色器选择</div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 顶部 Logo -->
|
|
|
+ <el-form-item label="顶部 Logo">
|
|
|
+ <div class="upload-group">
|
|
|
+ <div class="unified-upload-container">
|
|
|
+ <!-- 缩略图区域 -->
|
|
|
+ <div v-if="config.topLogo" class="thumbnail-area">
|
|
|
+ <div class="thumbnail-wrapper checkered-bg">
|
|
|
+ <img :src="config.topLogo" alt="顶部Logo" class="thumbnail-img" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 上传按钮区域 -->
|
|
|
+ <div class="uploader-box" @click="triggerTopLogoUpload">
|
|
|
+ <i class="el-icon-plus"></i>
|
|
|
+ <div class="upload-text">上传图片</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 隐藏的上传组件 -->
|
|
|
+ <input
|
|
|
+ ref="topLogoInput"
|
|
|
+ type="file"
|
|
|
+ accept="image/png"
|
|
|
+ @change="handleTopLogoUpload"
|
|
|
+ style="display: none"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="upload-actions">
|
|
|
+ <div class="upload-hint">推荐 100×50 / PNG,大小 < 1MB</div>
|
|
|
+ <el-button
|
|
|
+ v-if="config.topLogo"
|
|
|
+ type="text"
|
|
|
+ size="mini"
|
|
|
+ @click="confirmClearTopLogo"
|
|
|
+ class="clear-btn"
|
|
|
+ >
|
|
|
+ 清空图片
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 登录页 Logo -->
|
|
|
+ <el-form-item label="登录页 Logo">
|
|
|
+ <div class="upload-group">
|
|
|
+ <div class="unified-upload-container">
|
|
|
+ <!-- 缩略图区域 -->
|
|
|
+ <div v-if="config.loginLogo" class="thumbnail-area">
|
|
|
+ <div class="thumbnail-wrapper checkered-bg">
|
|
|
+ <img :src="config.loginLogo" alt="登录页Logo" class="thumbnail-img" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 上传按钮区域 -->
|
|
|
+ <div class="uploader-box" @click="triggerLoginLogoUpload">
|
|
|
+ <i class="el-icon-plus"></i>
|
|
|
+ <div class="upload-text">上传图片</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 隐藏的上传组件 -->
|
|
|
+ <input
|
|
|
+ ref="loginLogoInput"
|
|
|
+ type="file"
|
|
|
+ accept="image/png"
|
|
|
+ @change="handleLoginLogoUpload"
|
|
|
+ style="display: none"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="upload-actions">
|
|
|
+ <div class="upload-hint">推荐 200×100 / PNG,大小 < 1MB</div>
|
|
|
+ <el-button
|
|
|
+ v-if="config.loginLogo"
|
|
|
+ type="text"
|
|
|
+ size="mini"
|
|
|
+ @click="confirmClearLoginLogo"
|
|
|
+ class="clear-btn"
|
|
|
+ >
|
|
|
+ 清空图片
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 登录页背景图 -->
|
|
|
+ <el-form-item label="登录页背景图">
|
|
|
+ <div class="upload-group">
|
|
|
+ <div class="unified-upload-container">
|
|
|
+ <!-- 缩略图区域 -->
|
|
|
+ <div v-if="config.loginBg" class="thumbnail-area">
|
|
|
+ <div class="thumbnail-wrapper">
|
|
|
+ <img :src="config.loginBg" alt="登录页背景图" class="thumbnail-img" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 上传按钮区域 -->
|
|
|
+ <div class="uploader-box" @click="triggerLoginBgUpload">
|
|
|
+ <i class="el-icon-plus"></i>
|
|
|
+ <div class="upload-text">上传图片</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 隐藏的上传组件 -->
|
|
|
+ <input
|
|
|
+ ref="loginBgInput"
|
|
|
+ type="file"
|
|
|
+ accept="image/jpeg,image/jpg"
|
|
|
+ @change="handleLoginBgUpload"
|
|
|
+ style="display: none"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="upload-actions">
|
|
|
+ <div class="upload-hint">推荐 1290×1080 / JPG,大小 < 3MB</div>
|
|
|
+ <el-button
|
|
|
+ v-if="config.loginBg"
|
|
|
+ type="text"
|
|
|
+ size="mini"
|
|
|
+ @click="confirmClearLoginBg"
|
|
|
+ class="clear-btn"
|
|
|
+ >
|
|
|
+ 清空图片
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <!-- 吸底操作条 -->
|
|
|
+ <div class="sticky-actions">
|
|
|
+ <div class="actions-content">
|
|
|
+ <el-button
|
|
|
+ @click="resetDefault"
|
|
|
+ icon="el-icon-refresh"
|
|
|
+ :loading="resetting"
|
|
|
+ >
|
|
|
+ 重置默认
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ @click="saveConfig"
|
|
|
+ icon="el-icon-check"
|
|
|
+ :disabled="!isFormValid"
|
|
|
+ :loading="saving"
|
|
|
+ >
|
|
|
+ 保存
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ name: 'IndividuationPage',
|
|
|
+
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ // 配置状态
|
|
|
+ config: {
|
|
|
+ primaryColor: '#1677ff',
|
|
|
+ secondaryColor: '#52c41a',
|
|
|
+ topLogo: null,
|
|
|
+ loginLogo: null,
|
|
|
+ loginBg: null
|
|
|
+ },
|
|
|
+
|
|
|
+ // 颜色输入框的值(用于校验和格式化)
|
|
|
+ primaryColorInput: '1677ff',
|
|
|
+ secondaryColorInput: '52c41a',
|
|
|
+
|
|
|
+ // 默认配置
|
|
|
+ defaultConfig: {
|
|
|
+ primaryColor: '#1677ff',
|
|
|
+ secondaryColor: '#52c41a',
|
|
|
+ topLogo: null,
|
|
|
+ loginLogo: null,
|
|
|
+ loginBg: null
|
|
|
+ },
|
|
|
+
|
|
|
+ // 原始配置(用于检测是否有未保存更改)
|
|
|
+ originalConfig: null,
|
|
|
+
|
|
|
+ // 离开页面拦截
|
|
|
+ isLeaving: false,
|
|
|
+
|
|
|
+ // 按钮loading状态
|
|
|
+ saving: false,
|
|
|
+ resetting: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ computed: {
|
|
|
+ // 检查主色是否为有效的HEX格式
|
|
|
+ isPrimaryColorValid() {
|
|
|
+ return this.isValidHexColor(this.primaryColorInput)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 检查辅色是否为有效的HEX格式
|
|
|
+ isSecondaryColorValid() {
|
|
|
+ return this.isValidHexColor(this.secondaryColorInput)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 表单是否有效(颜色格式正确)
|
|
|
+ isFormValid() {
|
|
|
+ return this.isPrimaryColorValid && this.isSecondaryColorValid
|
|
|
+ },
|
|
|
+
|
|
|
+ // 是否有未保存的更改
|
|
|
+ hasUnsavedChanges() {
|
|
|
+ if (!this.originalConfig) return false
|
|
|
+ return JSON.stringify(this.config) !== JSON.stringify(this.originalConfig)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ created() {
|
|
|
+ this.loadFromServer()
|
|
|
+ this.applyTheme()
|
|
|
+ this.setupBeforeUnload()
|
|
|
+ },
|
|
|
+
|
|
|
+ beforeDestroy() {
|
|
|
+ this.removeBeforeUnload()
|
|
|
+ },
|
|
|
+
|
|
|
+ beforeRouteLeave(to, from, next) {
|
|
|
+ if (this.hasUnsavedChanges && !this.isLeaving) {
|
|
|
+ this.$confirm('当前修改未保存,是否确认离开?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ this.isLeaving = true
|
|
|
+ next()
|
|
|
+ }).catch(() => {
|
|
|
+ next(false)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ next()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ methods: {
|
|
|
+ // 应用主题到全局 CSS 变量
|
|
|
+ applyTheme() {
|
|
|
+ const root = document.documentElement
|
|
|
+ root.style.setProperty('--brand-primary', this.config.primaryColor)
|
|
|
+ root.style.setProperty('--brand-accent', this.config.secondaryColor)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 检查是否为有效的HEX颜色
|
|
|
+ isValidHexColor(hex) {
|
|
|
+ if (!hex) return false
|
|
|
+ // 移除可能的#前缀进行验证
|
|
|
+ const cleanHex = hex.replace('#', '')
|
|
|
+ // 验证3位或6位HEX格式
|
|
|
+ const hexRegex = /^([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/
|
|
|
+ return hexRegex.test(cleanHex)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 将3位HEX转换为6位
|
|
|
+ expandHex(hex) {
|
|
|
+ const cleanHex = hex.replace('#', '')
|
|
|
+ if (cleanHex.length === 3) {
|
|
|
+ return cleanHex.split('').map(char => char + char).join('')
|
|
|
+ }
|
|
|
+ return cleanHex
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // 主色输入变化处理
|
|
|
+ onPrimaryColorInputChange(value) {
|
|
|
+ // 只允许输入合法的HEX字符
|
|
|
+ const cleanValue = value.replace(/[^A-Fa-f0-9]/g, '').toLowerCase()
|
|
|
+ this.primaryColorInput = cleanValue
|
|
|
+
|
|
|
+ if (this.isValidHexColor(cleanValue)) {
|
|
|
+ const expandedHex = this.expandHex(cleanValue)
|
|
|
+ this.config.primaryColor = '#' + expandedHex
|
|
|
+ this.applyTheme()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 辅色输入变化处理
|
|
|
+ onSecondaryColorInputChange(value) {
|
|
|
+ // 只允许输入合法的HEX字符
|
|
|
+ const cleanValue = value.replace(/[^A-Fa-f0-9]/g, '').toLowerCase()
|
|
|
+ this.secondaryColorInput = cleanValue
|
|
|
+
|
|
|
+ if (this.isValidHexColor(cleanValue)) {
|
|
|
+ const expandedHex = this.expandHex(cleanValue)
|
|
|
+ this.config.secondaryColor = '#' + expandedHex
|
|
|
+ this.applyTheme()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 主色变化处理(取色器)
|
|
|
+ onPrimaryColorChange(value) {
|
|
|
+ if (value) {
|
|
|
+ this.config.primaryColor = value
|
|
|
+ this.primaryColorInput = value.replace('#', '')
|
|
|
+ this.applyTheme()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 辅色变化处理(取色器)
|
|
|
+ onSecondaryColorChange(value) {
|
|
|
+ if (value) {
|
|
|
+ this.config.secondaryColor = value
|
|
|
+ this.secondaryColorInput = value.replace('#', '')
|
|
|
+ this.applyTheme()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 触发顶部 Logo 上传
|
|
|
+ triggerTopLogoUpload() {
|
|
|
+ this.$refs.topLogoInput.click()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理顶部 Logo 上传
|
|
|
+ handleTopLogoUpload(event) {
|
|
|
+ const file = event.target.files[0]
|
|
|
+ if (!file) return
|
|
|
+
|
|
|
+ // 校验文件类型
|
|
|
+ if (file.type !== 'image/png') {
|
|
|
+ this.$message.error('顶部 Logo 只支持 PNG 格式!')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验文件大小
|
|
|
+ if (file.size / 1024 / 1024 > 1) {
|
|
|
+ this.$message.error('顶部 Logo 大小不能超过 1MB!')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取文件
|
|
|
+ const reader = new FileReader()
|
|
|
+ reader.onload = (e) => {
|
|
|
+ this.config.topLogo = e.target.result
|
|
|
+ this.applyTheme()
|
|
|
+ }
|
|
|
+ reader.readAsDataURL(file)
|
|
|
+
|
|
|
+ // 清空input值,允许重复选择同一文件
|
|
|
+ event.target.value = ''
|
|
|
+ },
|
|
|
+
|
|
|
+ // 顶部 Logo 上传前校验(保留兼容性)
|
|
|
+ beforeUploadTopLogo(file) {
|
|
|
+ const isValidType = file.type === 'image/png'
|
|
|
+ const isValidSize = file.size / 1024 / 1024 < 1 // 1MB
|
|
|
+
|
|
|
+ if (!isValidType) {
|
|
|
+ this.$message.error('顶部 Logo 只支持 PNG 格式!')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if (!isValidSize) {
|
|
|
+ this.$message.error('顶部 Logo 大小不能超过 1MB!')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return false // 阻止自动上传
|
|
|
+ },
|
|
|
+
|
|
|
+ // 顶部 Logo 变化处理
|
|
|
+ onTopLogoChange(file, fileList) {
|
|
|
+ if (file.status !== 'ready') return
|
|
|
+
|
|
|
+ const reader = new FileReader()
|
|
|
+ reader.onload = (e) => {
|
|
|
+ this.config.topLogo = e.target.result
|
|
|
+ this.topLogoFileList = [{
|
|
|
+ name: file.name,
|
|
|
+ url: e.target.result,
|
|
|
+ uid: file.uid
|
|
|
+ }]
|
|
|
+ this.applyTheme() // 图片变化也触发主题应用
|
|
|
+ }
|
|
|
+ reader.readAsDataURL(file.raw)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 顶部 Logo 移除处理
|
|
|
+ onTopLogoRemove() {
|
|
|
+ this.config.topLogo = null
|
|
|
+ this.topLogoFileList = []
|
|
|
+ this.applyTheme()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 确认清空顶部 Logo
|
|
|
+ async confirmClearTopLogo() {
|
|
|
+ try {
|
|
|
+ await this.$confirm('确认清空当前图片?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ this.clearTopLogo()
|
|
|
+ } catch (error) {
|
|
|
+ // 用户取消操作
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 清空顶部 Logo
|
|
|
+ clearTopLogo() {
|
|
|
+ this.config.topLogo = null
|
|
|
+ this.topLogoFileList = []
|
|
|
+ this.applyTheme()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 触发登录页 Logo 上传
|
|
|
+ triggerLoginLogoUpload() {
|
|
|
+ this.$refs.loginLogoInput.click()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理登录页 Logo 上传
|
|
|
+ handleLoginLogoUpload(event) {
|
|
|
+ const file = event.target.files[0]
|
|
|
+ if (!file) return
|
|
|
+
|
|
|
+ // 校验文件类型
|
|
|
+ if (file.type !== 'image/png') {
|
|
|
+ this.$message.error('登录页 Logo 只支持 PNG 格式!')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验文件大小
|
|
|
+ if (file.size / 1024 / 1024 > 1) {
|
|
|
+ this.$message.error('登录页 Logo 大小不能超过 1MB!')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取文件
|
|
|
+ const reader = new FileReader()
|
|
|
+ reader.onload = (e) => {
|
|
|
+ this.config.loginLogo = e.target.result
|
|
|
+ this.applyTheme()
|
|
|
+ }
|
|
|
+ reader.readAsDataURL(file)
|
|
|
+
|
|
|
+ // 清空input值,允许重复选择同一文件
|
|
|
+ event.target.value = ''
|
|
|
+ },
|
|
|
+
|
|
|
+ // 登录页 Logo 上传前校验(保留兼容性)
|
|
|
+ beforeUploadLoginLogo(file) {
|
|
|
+ const isValidType = file.type === 'image/png'
|
|
|
+ const isValidSize = file.size / 1024 / 1024 < 1 // 1MB
|
|
|
+
|
|
|
+ if (!isValidType) {
|
|
|
+ this.$message.error('登录页 Logo 只支持 PNG 格式!')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if (!isValidSize) {
|
|
|
+ this.$message.error('登录页 Logo 大小不能超过 1MB!')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return false // 阻止自动上传
|
|
|
+ },
|
|
|
+
|
|
|
+ // 登录页 Logo 变化处理
|
|
|
+ onLoginLogoChange(file, fileList) {
|
|
|
+ if (file.status !== 'ready') return
|
|
|
+
|
|
|
+ const reader = new FileReader()
|
|
|
+ reader.onload = (e) => {
|
|
|
+ this.config.loginLogo = e.target.result
|
|
|
+ this.loginLogoFileList = [{
|
|
|
+ name: file.name,
|
|
|
+ url: e.target.result,
|
|
|
+ uid: file.uid
|
|
|
+ }]
|
|
|
+ this.applyTheme()
|
|
|
+ }
|
|
|
+ reader.readAsDataURL(file.raw)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 登录页 Logo 移除处理
|
|
|
+ onLoginLogoRemove() {
|
|
|
+ this.config.loginLogo = null
|
|
|
+ this.loginLogoFileList = []
|
|
|
+ this.applyTheme()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 确认清空登录页 Logo
|
|
|
+ async confirmClearLoginLogo() {
|
|
|
+ try {
|
|
|
+ await this.$confirm('确认清空当前图片?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ this.clearLoginLogo()
|
|
|
+ } catch (error) {
|
|
|
+ // 用户取消操作
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 清空登录页 Logo
|
|
|
+ clearLoginLogo() {
|
|
|
+ this.config.loginLogo = null
|
|
|
+ this.loginLogoFileList = []
|
|
|
+ this.applyTheme()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 触发登录背景图上传
|
|
|
+ triggerLoginBgUpload() {
|
|
|
+ this.$refs.loginBgInput.click()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理登录背景图上传
|
|
|
+ handleLoginBgUpload(event) {
|
|
|
+ const file = event.target.files[0]
|
|
|
+ if (!file) return
|
|
|
+
|
|
|
+ // 校验文件类型
|
|
|
+ if (file.type !== 'image/jpeg' && file.type !== 'image/jpg') {
|
|
|
+ this.$message.error('登录背景图只支持 JPG/JPEG 格式!')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验文件大小
|
|
|
+ if (file.size / 1024 / 1024 > 3) {
|
|
|
+ this.$message.error('登录背景图大小不能超过 3MB!')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取文件
|
|
|
+ const reader = new FileReader()
|
|
|
+ reader.onload = (e) => {
|
|
|
+ this.config.loginBg = e.target.result
|
|
|
+ this.applyTheme()
|
|
|
+ }
|
|
|
+ reader.readAsDataURL(file)
|
|
|
+
|
|
|
+ // 清空input值,允许重复选择同一文件
|
|
|
+ event.target.value = ''
|
|
|
+ },
|
|
|
+
|
|
|
+ // 登录背景图上传前校验(保留兼容性)
|
|
|
+ beforeUploadLoginBg(file) {
|
|
|
+ const isValidType = file.type === 'image/jpeg' || file.type === 'image/jpg'
|
|
|
+ const isValidSize = file.size / 1024 / 1024 < 3 // 3MB
|
|
|
+
|
|
|
+ if (!isValidType) {
|
|
|
+ this.$message.error('登录背景图只支持 JPG/JPEG 格式!')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if (!isValidSize) {
|
|
|
+ this.$message.error('登录背景图大小不能超过 3MB!')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return false // 阻止自动上传
|
|
|
+ },
|
|
|
+
|
|
|
+ // 登录背景图变化处理
|
|
|
+ onLoginBgChange(file, fileList) {
|
|
|
+ if (file.status !== 'ready') return
|
|
|
+
|
|
|
+ const reader = new FileReader()
|
|
|
+ reader.onload = (e) => {
|
|
|
+ this.config.loginBg = e.target.result
|
|
|
+ this.loginBgFileList = [{
|
|
|
+ name: file.name,
|
|
|
+ url: e.target.result,
|
|
|
+ uid: file.uid
|
|
|
+ }]
|
|
|
+ this.applyTheme()
|
|
|
+ }
|
|
|
+ reader.readAsDataURL(file.raw)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 登录背景图移除处理
|
|
|
+ onLoginBgRemove() {
|
|
|
+ this.config.loginBg = null
|
|
|
+ this.loginBgFileList = []
|
|
|
+ this.applyTheme()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 确认清空登录背景图
|
|
|
+ async confirmClearLoginBg() {
|
|
|
+ try {
|
|
|
+ await this.$confirm('确认清空当前图片?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ this.clearLoginBg()
|
|
|
+ } catch (error) {
|
|
|
+ // 用户取消操作
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 清空登录背景图
|
|
|
+ clearLoginBg() {
|
|
|
+ this.config.loginBg = null
|
|
|
+ this.loginBgFileList = []
|
|
|
+ this.applyTheme()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 从后端加载配置
|
|
|
+ async loadFromServer() {
|
|
|
+ try {
|
|
|
+ // 调用后端API获取配置
|
|
|
+ // const response = await this.$axios.get('/api/individuation/config')
|
|
|
+ // const serverConfig = response.data
|
|
|
+ //
|
|
|
+ // 返回格式:
|
|
|
+ // {
|
|
|
+ // code: 200,
|
|
|
+ // message: "获取成功",
|
|
|
+ // data: {
|
|
|
+ // primaryColor: "#1677ff",
|
|
|
+ // secondaryColor: "#52c41a",
|
|
|
+ // topLogo: "/uploads/top-logo.png", // 文件路径或null
|
|
|
+ // loginLogo: "/uploads/login-logo.png", // 文件路径或null
|
|
|
+ // loginBg: "/uploads/login-bg.jpg" // 文件路径或null
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+
|
|
|
+ // 模拟API调用
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 500))
|
|
|
+
|
|
|
+ // 先尝试从本地存储加载(作为备用)
|
|
|
+ const saved = localStorage.getItem('individuationConfig')
|
|
|
+ let serverConfig = null
|
|
|
+
|
|
|
+ if (saved) {
|
|
|
+ serverConfig = JSON.parse(saved)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有服务器配置就使用,否则使用默认配置
|
|
|
+ if (serverConfig) {
|
|
|
+ this.config = { ...this.defaultConfig, ...serverConfig }
|
|
|
+ } else {
|
|
|
+ this.config = { ...this.defaultConfig }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 同步颜色输入框的值
|
|
|
+ this.primaryColorInput = this.config.primaryColor.replace('#', '')
|
|
|
+ this.secondaryColorInput = this.config.secondaryColor.replace('#', '')
|
|
|
+
|
|
|
+ // 记录原始配置用于检测更改
|
|
|
+ this.originalConfig = JSON.parse(JSON.stringify(this.config))
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载服务器配置失败:', error)
|
|
|
+ this.config = { ...this.defaultConfig }
|
|
|
+ this.originalConfig = JSON.parse(JSON.stringify(this.config))
|
|
|
+ this.$message.warning('加载配置失败,使用默认配置')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 保存配置到后端
|
|
|
+ async saveConfig() {
|
|
|
+ if (!this.isFormValid) {
|
|
|
+ this.$message.error('请检查颜色格式是否正确')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.saving = true
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 构造保存数据
|
|
|
+ const saveData = {
|
|
|
+ primaryColor: this.config.primaryColor,
|
|
|
+ secondaryColor: this.config.secondaryColor,
|
|
|
+ topLogo: this.config.topLogo,
|
|
|
+ loginLogo: this.config.loginLogo,
|
|
|
+ loginBg: this.config.loginBg
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用后端API保存配置
|
|
|
+ // const response = await this.$axios.post('/api/individuation/save', saveData)
|
|
|
+ //
|
|
|
+ // 接口参数说明:
|
|
|
+ // {
|
|
|
+ // primaryColor: string, // 主题主色,如 "#1677ff"
|
|
|
+ // secondaryColor: string, // 主题辅色,如 "#52c41a"
|
|
|
+ // topLogo: string | null, // 顶部Logo的Base64数据或上传后的文件路径
|
|
|
+ // loginLogo: string | null, // 登录页Logo的Base64数据或上传后的文件路径
|
|
|
+ // loginBg: string | null // 登录背景图的Base64数据或上传后的文件路径
|
|
|
+ // }
|
|
|
+ //
|
|
|
+ // 返回格式:
|
|
|
+ // {
|
|
|
+ // code: 200,
|
|
|
+ // message: "保存成功",
|
|
|
+ // data: {
|
|
|
+ // topLogoUrl: "/uploads/top-logo.png", // 如果上传了顶部Logo
|
|
|
+ // loginLogoUrl: "/uploads/login-logo.png", // 如果上传了登录Logo
|
|
|
+ // loginBgUrl: "/uploads/login-bg.jpg" // 如果上传了背景图
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+
|
|
|
+ // 模拟API调用
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 1000))
|
|
|
+
|
|
|
+ // 保存成功,同时保存到本地
|
|
|
+ localStorage.setItem('individuationConfig', JSON.stringify(this.config))
|
|
|
+
|
|
|
+ // 更新原始配置,清除未保存状态
|
|
|
+ this.originalConfig = JSON.parse(JSON.stringify(this.config))
|
|
|
+
|
|
|
+ this.$message.success('配置保存成功')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存配置失败:', error)
|
|
|
+ this.$message.error('保存失败:' + (error.message || '请稍后重试'))
|
|
|
+ } finally {
|
|
|
+ this.saving = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 重置为默认配置
|
|
|
+ async resetDefault() {
|
|
|
+ this.resetting = true
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 调用后端API重置为默认配置
|
|
|
+ // const response = await this.$axios.post('/api/individuation/reset')
|
|
|
+ // const defaultConfig = response.data
|
|
|
+ //
|
|
|
+ // 返回格式:
|
|
|
+ // {
|
|
|
+ // code: 200,
|
|
|
+ // message: "重置成功",
|
|
|
+ // data: {
|
|
|
+ // primaryColor: "#1677ff",
|
|
|
+ // secondaryColor: "#52c41a",
|
|
|
+ // topLogo: null,
|
|
|
+ // loginLogo: null,
|
|
|
+ // loginBg: null
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+
|
|
|
+ // 模拟API调用
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 800))
|
|
|
+
|
|
|
+ // 模拟后端返回的默认配置
|
|
|
+ const defaultConfig = {
|
|
|
+ primaryColor: '#1677ff',
|
|
|
+ secondaryColor: '#52c41a',
|
|
|
+ topLogo: null,
|
|
|
+ loginLogo: null,
|
|
|
+ loginBg: null
|
|
|
+ }
|
|
|
+
|
|
|
+ // 应用默认配置
|
|
|
+ this.config = { ...defaultConfig }
|
|
|
+ this.primaryColorInput = defaultConfig.primaryColor.replace('#', '')
|
|
|
+ this.secondaryColorInput = defaultConfig.secondaryColor.replace('#', '')
|
|
|
+
|
|
|
+ // 更新原始配置
|
|
|
+ this.originalConfig = JSON.parse(JSON.stringify(this.config))
|
|
|
+
|
|
|
+ // 应用主题
|
|
|
+ this.applyTheme()
|
|
|
+
|
|
|
+ // 同时更新本地存储
|
|
|
+ localStorage.setItem('individuationConfig', JSON.stringify(this.config))
|
|
|
+
|
|
|
+ this.$message.success('已重置为默认配置')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('重置配置失败:', error)
|
|
|
+ this.$message.error('重置失败:' + (error.message || '请稍后重试'))
|
|
|
+ } finally {
|
|
|
+ this.resetting = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 设置页面离开拦截
|
|
|
+ setupBeforeUnload() {
|
|
|
+ this.beforeUnloadHandler = (event) => {
|
|
|
+ if (this.hasUnsavedChanges && !this.isLeaving) {
|
|
|
+ event.preventDefault()
|
|
|
+ event.returnValue = '当前修改未保存,是否确认离开?'
|
|
|
+ return '当前修改未保存,是否确认离开?'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ window.addEventListener('beforeunload', this.beforeUnloadHandler)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 移除页面离开拦截
|
|
|
+ removeBeforeUnload() {
|
|
|
+ if (this.beforeUnloadHandler) {
|
|
|
+ window.removeEventListener('beforeunload', this.beforeUnloadHandler)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.individuation-page {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: var(--color-bg-secondary);
|
|
|
+ /* 移除多余的底部间距,卡片内置底栏不需要额外空间 */
|
|
|
+ padding-bottom: var(--spacing-4);
|
|
|
+
|
|
|
+ .page-header {
|
|
|
+ background: var(--color-bg-card);
|
|
|
+ border-bottom: 1px solid var(--color-border-primary);
|
|
|
+ padding: var(--spacing-6);
|
|
|
+ margin-bottom: var(--spacing-6);
|
|
|
+ box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
|
|
|
+
|
|
|
+ .header-content {
|
|
|
+ max-width: 1200px;
|
|
|
+ margin: 0 auto;
|
|
|
+
|
|
|
+ .header-main {
|
|
|
+ .page-title {
|
|
|
+ color: var(--color-text-primary);
|
|
|
+ font-size: var(--font-size-3xl);
|
|
|
+ font-weight: var(--font-weight-bold);
|
|
|
+ margin: 0 0 var(--spacing-2) 0;
|
|
|
+ line-height: var(--line-height-tight);
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-desc {
|
|
|
+ color: var(--color-text-secondary);
|
|
|
+ font-size: var(--font-size-base);
|
|
|
+ margin: 0;
|
|
|
+ line-height: var(--line-height-relaxed);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-content {
|
|
|
+ max-width: 800px;
|
|
|
+ margin: 0 auto;
|
|
|
+ padding: 0 var(--spacing-6);
|
|
|
+ /* 确保不会裁剪卡片阴影 */
|
|
|
+ overflow: visible;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 未保存更改警告条 */
|
|
|
+.unsaved-warning {
|
|
|
+ background: #fff7e6;
|
|
|
+ border: 1px solid #ffd591;
|
|
|
+ border-radius: var(--radius-lg);
|
|
|
+ margin: 0 auto var(--spacing-4);
|
|
|
+ max-width: 1200px;
|
|
|
+
|
|
|
+ .warning-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: var(--spacing-3) var(--spacing-6);
|
|
|
+
|
|
|
+ i {
|
|
|
+ color: #fa8c16;
|
|
|
+ font-size: 16px;
|
|
|
+ margin-right: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ span {
|
|
|
+ color: #d46b08;
|
|
|
+ font-size: 14px;
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ margin-left: 12px;
|
|
|
+ color: #d46b08;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #ad4e00;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 配置卡片 */
|
|
|
+.config-card {
|
|
|
+ background: var(--color-bg-card);
|
|
|
+ border-radius: var(--radius-xl);
|
|
|
+ box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
|
|
|
+ border: 1px solid var(--color-border-primary);
|
|
|
+ transition: all var(--duration-200) var(--ease-out);
|
|
|
+ animation: slideInUp 0.3s ease-out;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ /* 确保卡片内容能够继承圆角,底栏与卡片融为一体 */
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ box-shadow: 0 8px 24px rgba(2, 6, 23, 0.08);
|
|
|
+ border-color: var(--color-border-secondary);
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-header {
|
|
|
+ padding: var(--spacing-5);
|
|
|
+ border-bottom: 1px solid var(--color-border-secondary);
|
|
|
+
|
|
|
+ .card-title {
|
|
|
+ font-size: var(--font-size-lg);
|
|
|
+ font-weight: var(--font-weight-semibold);
|
|
|
+ color: var(--color-text-primary);
|
|
|
+ margin: 0 0 var(--spacing-1) 0;
|
|
|
+ line-height: var(--line-height-tight);
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-desc {
|
|
|
+ font-size: var(--font-size-sm);
|
|
|
+ color: var(--color-text-tertiary);
|
|
|
+ margin: 0;
|
|
|
+ line-height: var(--line-height-relaxed);
|
|
|
+ opacity: 0.8;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-content {
|
|
|
+ padding: var(--spacing-5);
|
|
|
+ /* 移除底部额外间距,让内容与底栏紧密连接 */
|
|
|
+ padding-bottom: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 表单样式 */
|
|
|
+.config-form {
|
|
|
+ /* 为表单内容添加底部边距,与卡片底栏形成自然间隔 */
|
|
|
+ padding-bottom: var(--spacing-4);
|
|
|
+ .color-form-item {
|
|
|
+ .color-input-group {
|
|
|
+ .color-input {
|
|
|
+ width: 300px;
|
|
|
+
|
|
|
+ &.input-error {
|
|
|
+ ::v-deep .el-input__inner {
|
|
|
+ border-color: var(--color-danger) !important;
|
|
|
+ box-shadow: 0 0 0 2px rgba(245, 34, 45, 0.2) !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-group {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 16px;
|
|
|
+
|
|
|
+ .upload-actions {
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ .upload-hint {
|
|
|
+ color: #999;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.5;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .clear-btn {
|
|
|
+ color: var(--color-danger);
|
|
|
+ font-size: 12px;
|
|
|
+ padding: 0;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: var(--color-danger-light);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 统一上传容器 */
|
|
|
+ .unified-upload-container {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 缩略图区域 */
|
|
|
+ .thumbnail-area {
|
|
|
+ .thumbnail-wrapper {
|
|
|
+ width: 120px;
|
|
|
+ height: 120px;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid var(--color-border-secondary);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+
|
|
|
+ &.checkered-bg {
|
|
|
+ background-image:
|
|
|
+ linear-gradient(45deg, #f5f5f5 25%, transparent 25%),
|
|
|
+ linear-gradient(-45deg, #f5f5f5 25%, transparent 25%),
|
|
|
+ linear-gradient(45deg, transparent 75%, #f5f5f5 75%),
|
|
|
+ linear-gradient(-45deg, transparent 75%, #f5f5f5 75%);
|
|
|
+ background-size: 12px 12px;
|
|
|
+ background-position: 0 0, 0 6px, 6px -6px, -6px 0px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .thumbnail-img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 统一上传按钮样式 */
|
|
|
+ .uploader-box {
|
|
|
+ width: 120px;
|
|
|
+ height: 120px;
|
|
|
+ border: 2px dashed var(--color-border-secondary);
|
|
|
+ border-radius: 8px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ background-color: var(--color-bg-card);
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: var(--color-primary);
|
|
|
+ background-color: var(--color-bg-secondary);
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-icon-plus {
|
|
|
+ font-size: 24px;
|
|
|
+ color: var(--color-text-tertiary);
|
|
|
+ margin-bottom: 8px;
|
|
|
+ transition: color 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--color-text-tertiary);
|
|
|
+ transition: color 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ .el-icon-plus,
|
|
|
+ .upload-text {
|
|
|
+ color: var(--color-primary);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 隐藏的上传组件 */
|
|
|
+ .hidden-upload {
|
|
|
+ display: none !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-hint {
|
|
|
+ color: #999;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-top: 4px;
|
|
|
+ line-height: 1.4;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 表单项间距 */
|
|
|
+::v-deep .el-form-item {
|
|
|
+ margin-bottom: 16px;
|
|
|
+
|
|
|
+ .el-form-item__label {
|
|
|
+ color: var(--color-text-primary);
|
|
|
+ font-weight: var(--font-weight-medium);
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 卡片内置底栏 */
|
|
|
+.sticky-actions {
|
|
|
+ /* 移除独立吸底定位,改为卡片内部底栏 */
|
|
|
+ position: static;
|
|
|
+
|
|
|
+ /* 与卡片融为一体的样式 */
|
|
|
+ background: var(--color-bg-card);
|
|
|
+ border-top: 1px solid var(--color-border-secondary);
|
|
|
+
|
|
|
+ /* 移除独立阴影和独立圆角,与卡片共享 */
|
|
|
+ box-shadow: none;
|
|
|
+ border-radius: 0;
|
|
|
+
|
|
|
+ /* 与卡片内容保持一致的内边距 */
|
|
|
+ padding: var(--spacing-4) var(--spacing-5);
|
|
|
+ margin: 0 calc(-1 * var(--spacing-5));
|
|
|
+ margin-top: var(--spacing-4);
|
|
|
+
|
|
|
+ .actions-content {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ min-width: 100px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 上传组件样式优化 */
|
|
|
+::v-deep .el-upload--picture-card {
|
|
|
+ width: 100px;
|
|
|
+ height: 100px;
|
|
|
+ border-radius: 6px;
|
|
|
+ display: flex !important;
|
|
|
+ justify-content: center !important;
|
|
|
+ align-items: center !important;
|
|
|
+
|
|
|
+ .el-icon-plus {
|
|
|
+ font-size: 24px;
|
|
|
+ color: var(--color-text-tertiary);
|
|
|
+ margin: 0 !important;
|
|
|
+ line-height: 1 !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 上传列表样式 */
|
|
|
+::v-deep .el-upload-list--picture-card {
|
|
|
+ .el-upload-list__item {
|
|
|
+ width: 100px;
|
|
|
+ height: 100px;
|
|
|
+ border-radius: 6px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
+ transform: translateY(-2px);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-upload-list__item-thumbnail {
|
|
|
+ object-fit: contain;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 确保上传按钮完全居中 */
|
|
|
+::v-deep .el-upload {
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ &.el-upload--picture-card {
|
|
|
+ /* 重置所有可能影响居中的样式 */
|
|
|
+ text-align: center !important;
|
|
|
+ vertical-align: middle !important;
|
|
|
+
|
|
|
+ /* 确保内容居中 */
|
|
|
+ > * {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-icon-plus {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ margin: 0 !important;
|
|
|
+ padding: 0 !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 棋盘格背景 */
|
|
|
+.checkered-bg {
|
|
|
+ ::v-deep .el-upload--picture-card,
|
|
|
+ ::v-deep .el-upload-list__item {
|
|
|
+ background-image:
|
|
|
+ linear-gradient(45deg, #f5f5f5 25%, transparent 25%),
|
|
|
+ linear-gradient(-45deg, #f5f5f5 25%, transparent 25%),
|
|
|
+ linear-gradient(45deg, transparent 75%, #f5f5f5 75%),
|
|
|
+ linear-gradient(-45deg, transparent 75%, #f5f5f5 75%);
|
|
|
+ background-size: 12px 12px;
|
|
|
+ background-position: 0 0, 0 6px, 6px -6px, -6px 0px;
|
|
|
+
|
|
|
+ /* 确保棋盘格背景不影响居中 */
|
|
|
+ display: flex !important;
|
|
|
+ justify-content: center !important;
|
|
|
+ align-items: center !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ ::v-deep .el-upload-list__item-thumbnail {
|
|
|
+ background: transparent;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 棋盘格背景下的上传按钮居中 */
|
|
|
+ ::v-deep .el-upload--picture-card {
|
|
|
+ .el-icon-plus {
|
|
|
+ background: rgba(255, 255, 255, 0.8);
|
|
|
+ border-radius: 50%;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 颜色选择器样式 */
|
|
|
+::v-deep .el-color-picker {
|
|
|
+ .el-color-picker__trigger {
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 取色器在输入框右侧的样式 */
|
|
|
+::v-deep .el-input-group__append {
|
|
|
+ .color-picker-append {
|
|
|
+ .el-color-picker__trigger {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 主题变量验证 */
|
|
|
+.brand-primary {
|
|
|
+ color: var(--brand-primary);
|
|
|
+}
|
|
|
+
|
|
|
+.brand-accent {
|
|
|
+ color: var(--brand-accent);
|
|
|
+}
|
|
|
+
|
|
|
+// 滑入动画
|
|
|
+@keyframes slideInUp {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(20px);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式适配 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .individuation-page {
|
|
|
+ .page-header {
|
|
|
+ padding: var(--spacing-4);
|
|
|
+ margin-bottom: var(--spacing-4);
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-content {
|
|
|
+ padding: 0 var(--spacing-4);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .config-card {
|
|
|
+ .card-header {
|
|
|
+ padding: var(--spacing-4);
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-content {
|
|
|
+ padding: var(--spacing-4);
|
|
|
+ padding-bottom: 80px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .config-form {
|
|
|
+ .color-input-group {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 8px;
|
|
|
+
|
|
|
+ .color-input {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-group {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .sticky-actions {
|
|
|
+ /* 移动端也是卡片内部底栏,无需特殊定位 */
|
|
|
+ padding: var(--spacing-3) var(--spacing-4);
|
|
|
+ margin: 0 calc(-1 * var(--spacing-4));
|
|
|
+ margin-top: var(--spacing-3);
|
|
|
+
|
|
|
+ .actions-content {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 暗色主题适配 */
|
|
|
+html.dark {
|
|
|
+ .individuation-page {
|
|
|
+ .page-header {
|
|
|
+ background: var(--color-bg-tertiary);
|
|
|
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .unsaved-warning {
|
|
|
+ background: rgba(255, 247, 230, 0.1);
|
|
|
+ border-color: rgba(255, 213, 145, 0.3);
|
|
|
+
|
|
|
+ .warning-content {
|
|
|
+ i {
|
|
|
+ color: #ffa940;
|
|
|
+ }
|
|
|
+
|
|
|
+ span {
|
|
|
+ color: #ffa940;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ color: #ffa940;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #ffbb96;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .config-card {
|
|
|
+ background: var(--color-bg-tertiary);
|
|
|
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
|
|
|
+ border-color: var(--color-border-tertiary);
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
|
|
+ border-color: var(--color-border-secondary);
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-header {
|
|
|
+ border-bottom-color: var(--color-border-tertiary);
|
|
|
+
|
|
|
+ .card-title {
|
|
|
+ color: var(--color-text-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-desc {
|
|
|
+ color: var(--color-text-quaternary);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .sticky-actions {
|
|
|
+ /* 暗色主题下的卡片内置底栏 */
|
|
|
+ background: var(--color-bg-tertiary);
|
|
|
+ border-top-color: var(--color-border-tertiary);
|
|
|
+ /* 移除独立阴影,与卡片共享阴影 */
|
|
|
+ box-shadow: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-actions {
|
|
|
+ .upload-hint {
|
|
|
+ color: var(--color-text-quaternary);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-hint {
|
|
|
+ color: var(--color-text-quaternary);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 暗色主题下的缩略图区域 */
|
|
|
+ .thumbnail-area {
|
|
|
+ .thumbnail-wrapper {
|
|
|
+ &.checkered-bg {
|
|
|
+ background-image:
|
|
|
+ linear-gradient(45deg, #3a3a3a 25%, transparent 25%),
|
|
|
+ linear-gradient(-45deg, #3a3a3a 25%, transparent 25%),
|
|
|
+ linear-gradient(45deg, transparent 75%, #3a3a3a 75%),
|
|
|
+ linear-gradient(-45deg, transparent 75%, #3a3a3a 75%);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 暗色主题下的上传按钮 */
|
|
|
+ .uploader-box {
|
|
|
+ background-color: var(--color-bg-tertiary);
|
|
|
+ border-color: var(--color-border-tertiary);
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: var(--color-bg-secondary);
|
|
|
+ border-color: var(--color-primary);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|