点击submitForm没反应 - CSDN文库",
"datePublished": "2025-07-23",
"keywords": "
{{ form.sourceLabel || '请选择购课来源' }}
{{item.name}}
¥{{item.money}}
私教:{{item.personaltainerTimes}}节
小班:{{item.publicTimes}}节
有效天数:{{item.publicTimes}}
提交
点击submitForm没反应
",
"description": "文章浏览阅读6次。我们正在处理一个关于uni-app表单提交无反应的问题。用户描述说:在提交表单时没有反应,需要排查问题,特别是submitForm无法触发验证或提交。 根据提供的引用,我们可以总结以下几点: 1. 引用[1]提到,uni-form没有默认的submit方式,提交前的验证是通过调用`this.$refs.validForm.validate()`方法来实现的"
}
首页<template>
<view class="container">
<view class="content">
<u-form :model="form" ref="form" :rules="rules" label-width="150">
<!-- 购课来源(选择器形式) -->
<u-form-item label="购课来源" prop="sourceId" required>
<view class="custom-select" @click="showSourcePicker = true">
<text class="select-text" :class="{placeholder: !form.sourceLabel}">
{{ form.sourceLabel || '请选择购课来源' }}
</text>
<u-icon name="arrow-down" color="#999" size="28"></u-icon>
</view>
<u-picker :show="showSourcePicker" :columns="[sourceOptions]" keyName="sourceName"
@confirm="onSourceConfirm" @cancel="showSourcePicker = false;">
</u-picker>
</u-form-item>
<!-- 套餐选择 -->
<u-form-item v-if="packageOptions.length > 0" label="课程套餐" prop="courseMenuId" required>
<view class="package-list">
<view class="package-item" v-for="item in packageOptions" :key="item.id"
:class="{'package-item-active': form.courseMenuId === item.id}"
@click="form.courseMenuId = item.id">
<view class="package-name">{{item.name}}</view>
<view class="package-price">¥{{item.money}}</view>
<view class="package-count">私教:{{item.personaltainerTimes}}节</view>
<view class="package-count">小班:{{item.publicTimes}}节</view>
<view class="package-count">有效天数:{{item.publicTimes}}</view>
<u-icon v-if="form.courseMenuId === item.id" name="checkbox-mark" color="#7FB8A1" size="24"
class="package-check"></u-icon>
</view>
</view>
</u-form-item>
<!-- 定金开关 -->
<u-form-item label="定金订单" prop="isEarnest">
<u-switch v-model="form.isEarnest" activeValue="1" inactiveValue="0"
active-color="#7FB8A1"></u-switch>
</u-form-item>
</u-form>
<!-- 提交按钮 -->
<view class="submit-btn">
<u-button type="primary" shape="circle" @click="submitForm" :custom-style="buttonStyle">
提交
</u-button>
</view>
</view>
</view>
</template>
<script>
import {
buySourceInfo,
courseMenuInfoDic,
sale
} from "@/api/purchase-course.js"
export default {
data() {
return {
navbarBackground: {
backgroundColor: '#7FB8A1'
},
buttonStyle: {
backgroundColor: '#7FB8A1',
color: '#FFFFFF',
border: 'none'
},
showSourcePicker: false,
form: {
sourceId: '',
sourceLabel: '',
courseMenuId: '',
isEarnest: false,
qrCode: ''
},
sourceOptions: [],
packageOptions: [],
rules: {
sourceId: [{
required: true,
message: '请选择购课来源',
trigger: 'change'
}],
courseMenuId: [{
required: true,
message: '请选择课程套餐',
trigger: 'change'
}]
}
};
},
onLoad(option) {
console.log(option);
this.form.qrCode = option.result
},
onReady() {
// this.$refs.uForm.setRules(this.rules);
},
mounted() {
this.getBuySourceInfo()
},
methods: {
// 获取购课来源
getBuySourceInfo() {
buySourceInfo().then((res) => {
if (res.code == 200) {
this.sourceOptions = res.data
}
})
},
// 选择购课来源
onSourceConfirm(e) {
console.log(e);
this.form.sourceId = e.value[0].id
this.form.sourceLabel = e.value[0].sourceName
this.getCourseMenuInfoDic(this.form.sourceId)
this.showSourcePicker = false;
},
// 获取课程套餐
getCourseMenuInfoDic(sourceId) {
courseMenuInfoDic(sourceId).then((res) => {
if (res.code == 200) {
this.packageOptions = res.data
}
})
},
// 提交售课
submitForm() {
console.log('开始提交表单');
this.$refs.form.validate(valid => {
if (valid) {
const {
sourceLabel,
...buyRecordAdd
} = this.form
sale(buyRecordAdd).then((res) => {
if (res.code == 200) {
setTimeout(() => {
uni.showToast({
title: '提交成功',
icon: 'success'
});
console.log('表单数据:', buyRecordAdd);
}, 1500);
}
})
} else {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
});
}
})
}
}
};
</script>
<style lang="scss" scoped>
.container {
background-color: #f8f8f8;
min-height: 100vh;
}
.content {
padding: 20rpx 30rpx;
}
.custom-select {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
background-color: #fff;
border: 1px solid #eee;
border-radius: 8rpx;
.select-text {
font-size: 28rpx;
color: #333;
&.placeholder {
color: #999;
}
}
}
.package-list {
display: flex;
flex-wrap: wrap;
margin: -10rpx;
}
.package-item {
width: 100%;
margin: 10rpx;
padding: 20rpx;
background-color: #fff;
border-radius: 12rpx;
position: relative;
border: 1px solid #eee;
&-active {
border-color: #7FB8A1;
background-color: rgba(127, 184, 161, 0.05);
}
}
.package-name {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 10rpx;
color: #333;
}
.package-price {
font-size: 32rpx;
color: #7FB8A1;
font-weight: bold;
margin-bottom: 5rpx;
}
.package-count {
font-size: 24rpx;
color: #999;
}
.package-check {
position: absolute;
right: 10rpx;
top: 10rpx;
}
.submit-btn {
margin: 40rpx 0;
}
</style>点击submitForm没反应
<think>我们正在处理一个关于uni-app表单提交无反应的问题。用户描述说:在提交表单时没有反应,需要排查问题,特别是submitForm无法触发验证或提交。
根据提供的引用,我们可以总结以下几点:
1. 引用[1]提到,uni-form没有默认的submit方式,提交前的验证是通过调用`this.$refs.validForm.validate()`方法来实现的。这说明我们需要通过手动调用验证方法,然后根据验证结果来决定是否提交。
2. 引用[2]和[3]展示了表单提交后重置表单的方法,其中引用[3]使用了一个隐藏的重置按钮,并通过代码触发其点击事件来重置表单。
用户的问题集中在表单提交无反应,可能的原因包括:
- 表单绑定的事件是否正确(例如,是否绑定了submit事件)
- 提交按钮的form-type属性是否正确(应该为submit)
- 验证逻辑是否正确,是否因为验证失败而没有任何提示导致用户以为无反应
- 是否在提交过程中有异步操作未正确处理
我们按照以下步骤来排查:
步骤1:检查表单结构
确保表单使用`<form>`标签,并且提交按钮的`form-type`设置为"submit"。例如:
```html
<form @submit="formSubmit">
<!-- 表单项 -->
<button form-type="submit">提交</button>
</form>
```
步骤2:检查提交事件处理函数
在formSubmit方法中,应该处理表单提交逻辑。注意,uni-app表单提交事件中,表单数据可以通过`e.detail.value`获取。
步骤3:验证逻辑
如果使用了表单验证(如uni-forms组件),需要检查是否在提交事件中调用了验证方法。根据引用[1],验证是通过调用`validate()`方法实现的,这个方法返回一个Promise,所以需要正确处理then和catch。
步骤4:重置表单
如果需要重置表单,可以参考引用[3]的方法,使用一个隐藏的重置按钮,然后在提交完成后通过ref触发其点击事件。
步骤5:检查异步操作
如果提交中包含异步请求(如网络请求),确保在请求完成后有相应的反馈(如提示用户成功或失败),避免用户误以为无反应。
下面是一个示例代码,结合了验证和重置:
```html
<template>
<view>
<uni-forms ref="validForm" :model="formData">
<!-- 表单项,例如: -->
<uni-forms-item label="姓名" name="name">
<input v-model="formData.name" />
</uni-forms-item>
</uni-forms>
<button @click="submitForm">提交</button>
<!-- 隐藏的重置按钮 -->
<button ref="resetButton" form-type="reset" style="display:none;">重置</button>
</view>
</template>
<script>
export default {
data() {
return {
formData: {
name: ''
}
};
},
methods: {
submitForm() {
// 手动验证表单
this.$refs.validForm.validate().then(res => {
console.log('验证通过', res);
// 这里可以执行提交操作,例如发送请求
uni.request({
url: '...',
method: 'POST',
data: this.formData,
success: (res) => {
uni.showToast({ title: '提交成功' });
// 提交成功后重置表单
this.$refs.resetButton.$el.click();
},
fail: (err) => {
uni.showToast({ title: '提交失败', icon: 'error' });
}
});
}).catch(err => {
console.log('验证失败', err);
uni.showToast({ title: '验证失败,请检查输入', icon: 'none' });
});
}
}
};
</script>
```
注意:上面的代码中,我们使用了`<uni-forms>`组件,并给其设置ref为`validForm`。提交按钮不是放在form标签内部,而是单独放在外面,通过点击事件触发验证和提交。这是因为uni-forms组件并不依赖原生的form提交,而是通过自己的验证机制。
如果用户使用的是原生的form,那么应该按照以下方式:
```html
<template>
<view>
<form @submit="formSubmit">
<input name="name" placeholder="请输入姓名" />
<button form-type="submit">提交</button>
<button ref="resetButton" form-type="reset" style="display:none;">重置</button>
</form>
</view>
</template>
<script>
export default {
methods: {
formSubmit(e) {
console.log('表单提交数据:', e.detail.value);
// 这里发送请求
uni.request({
url: '...',
method: 'POST',
data: e.detail.value,
success: (res) => {
uni.showToast({ title: '提交成功' });
// 重置表单
this.$refs.resetButton.$el.click();
},
fail: (err) => {
uni.showToast({ title: '提交失败', icon: 'error' });
}
});
}
}
};
</script>
```
但是注意,原生的form提交没有内置的验证功能,所以如果需要验证,可能需要自己写验证逻辑,或者结合uni-forms组件。
因此,用户的问题可能是由于没有正确绑定事件、没有处理验证逻辑、或者验证失败没有提示等原因造成的。请用户检查以上几点。
如果用户使用了uni-forms组件,那么需要确保在提交时调用validate方法,并且处理其返回的Promise。如果验证失败,应该给用户提示,否则用户可能不知道表单验证未通过。
另外,如果用户使用的是vue3的setup语法,获取ref的方式会有所不同,如引用[3]所示,使用`resetButton.value.$el.click()`。
最后,如果按照以上步骤检查后问题仍然存在,请检查uni-app的版本,以及是否有其他错误信息输出到控制台。
希望这些步骤能帮助用户解决问题。</think>### Uni-app 表单提交无反应问题排查指南
当 Uni-app 表单提交无反应时,可能是以下原因导致。请按步骤排查:
#### 1. **检查表单基础结构**(最常见问题)
```html
<!-- 正确结构 -->
<form @submit="formSubmit">
<input name="username" placeholder="用户名" />
<button form-type="submit">提交</button> <!-- 关键属性 -->
</form>
```
- 确保按钮有 `form-type="submit"` 属性
- 表单需绑定 `@submit` 事件而非 `@click`
- 检查控制台是否有 `[Vue warn]` 错误提示
#### 2. **验证逻辑问题**(使用 uni-forms 时)
引用[1]中的验证方式:
```javascript
submit() {
this.$refs.validForm.validate().then(res => {
console.log('验证成功', res)
this.realSubmit() // 执行实际提交
}).catch(err => {
console.log('验证失败', err)
uni.showToast({ title: '请检查表单', icon: 'none' })
})
}
```
- 检查 `$refs` 名称是否与模板一致
- 确保 `.validate()` 返回 Promise 已正确处理
- 添加失败回调避免静默失败
#### 3. **表单重置干扰**(隐蔽问题)
引用[3]中提到的重置陷阱:
```html
<button ref="resetBtn" form-type="reset" style="display:none"></button>
```
```javascript
formSubmit() {
// 错误示例:提交后立即重置
this.$refs.resetBtn.$el.click() // 会导致提交中断
}
```
**解决方案:**
```javascript
formSubmit() {
// 先完成提交逻辑
api.submit().then(() => {
// 提交成功后再重置
this.$refs.resetBtn.$el.click()
})
}
```
#### 4. **异步请求未处理**
```javascript
async formSubmit(e) {
const formData = e.detail.value
try {
// 添加加载提示
uni.showLoading({ title: '提交中' })
// 异步请求必须等待完成
await uni.request({
url: '/api/submit',
method: 'POST',
data: formData
})
uni.showToast({ title: '提交成功' })
} catch (err) {
uni.showToast({ title: '提交失败', icon: 'error' })
} finally {
uni.hideLoading()
}
}
```
#### 5. **特殊场景排查**
- **自定义组件**:确保子组件使用 `:name="fieldName"` 传递字段名
- **H5 与其他端差异**:在 `manifest.json` 中添加:
```json
"h5": {
"template": {
"compileOptions": {
"isCustomElement": tag => tag === 'form'
}
}
}
```
- **微信小程序**:检查 `request` 域名是否在白名单
#### 6. **调试建议**
1. 添加基础日志:
```javascript
formSubmit(e) {
console.log('事件已触发', e) // 确认事件是否触发
console.log('表单数据', e.detail.value) // 验证数据获取
}
```
2. 使用模拟数据绕过验证:
```javascript
this.$refs.form.validate = () => Promise.resolve() // 临时跳过验证
```
> **经验提示**:表单提交无反应时,80% 的问题源于:① 缺少 `form-type="submit"` ② 验证失败未处理 ③ 异步操作未正确等待。优先检查这三个方面[^1][^3]。
---
### 相关问题
1. Uni-app 中如何实现表单字段的实时验证?
2. 表单提交成功后如何自动清空输入框?
3. Uni-app 的 `uni-forms` 组件与原生 `form` 标签有何区别?
4. 如何处理 Uni-app 表单在多端的兼容性问题?
5. 如何防止用户重复提交表单?
[^1]: 在使用uni-app的时候,uni-form实际上没有组件默认的submit方式,实际上在提交前的验证都是通过方法形如: submit(ref) { this.$refs.validForm.validate().then(res => { console.log('success', res); uni.showToast({ title: `校验通过` }) }).catch(err => { console.log('err', err); }) }
[^2]: uni-app踩坑 - 官方form组件 表单提交后实现手动清空数据。js: formSubmit(e) { console.log('form发生了submit事件,携带数据为:' + JSON.stringify(e.detail.value)) this.$refs.resrtBtn.$dispatch('Form', 'uni-form-reset', { type: 'reset' }) }
[^3]: uni-app 提交完表单,自动重置表单。 那么 refObj.value.$el.click() 自然能模拟触发重置按钮的点击行为了。
相关推荐

<template>
<view class="detail-container">
<u-card :border="false" margin="0 0 20rpx 0">
<view slot="body">
<u-form :model="formData" :rules="rules" ref="indexForm" class="mobile-form">
<u-form-item label="问卷标题:" prop="dcWjTitle" label-width="150rpx">
<u-input
v-model="formData.dcWjTitle"
placeholder="请输入问卷标题"
clearable
border="bottom"
prefixIcon="file-text"
/>
</u-form-item>
<u-form-item label="被测评人:" prop="dcId" label-width="150rpx">
<u-select
v-model="formData.dcId"
:list="bdrOptions"
multiple
value-name="dcId"
label-name="dcName"
@confirm="handleBdrSelect"
placeholder="请选择被测评人"
></u-select>
</u-form-item>
<u-form-item label="人员部门:" prop="dcDept" label-width="150rpx">
<u-input
v-model="formData.dcDept"
placeholder="请输入人员部门"
clearable
border="bottom"
prefixIcon="home"
/>
</u-form-item>
<u-form-item label="人员状态:" prop="state" label-width="150rpx">
<u-select
v-model="formData.state"
:list="stateOptions"
value-name="value"
label-name="label"
placeholder="请选择提交状态"
></u-select>
</u-form-item>
<view class="button-group">
<u-button
type="primary"
@click="handleSearch"
class="action-button"
icon="search"
>
搜索
</u-button>
<u-button
@click="handleReset"
class="action-button"
icon="reload"
>
重置
</u-button>
</view>
</u-form>
</view>
</u-card>
<u-card :border="false">
<view slot="head" class="card-header">
<u-button
type="primary"
size="small"
icon="reload"
@click="fetchData"
>
刷新数据
</u-button>
<u-search
v-model="searchKeyword"
placeholder="搜索问卷"
@search="handleKeywordSearch"
margin="0 0 0 20rpx"
/>
</view>
<scroll-view
scroll-y
style="height: calc(100vh - 400rpx);"
@refresherrefresh="onRefresh"
:refresher-enabled="true"
:refresher-triggered="refreshing"
>
<u-loading-page :loading="loading" loadingText="加载中..." />
<u-card
v-for="(item, index) in tableData"
:key="item.dcWjId"
:title="item.dcWjTitle"
:sub-title="创建时间: ${item.createTime}"
:border="false"
margin="0 0 20rpx 0"
>
<view class="u-body-item">
<view class="card-row">
<text class="card-label">被测评人:</text>
<text class="card-value">{{ item.dcName }}</text>
</view>
<view class="card-row">
<text class="card-label">部门:</text>
<text class="card-value">{{ item.dcDept }}</text>
</view>
<view class="card-row">
<text class="card-label">提交时间:</text>
<text class="card-value">{{ item.updateTime || '-' }}</text>
</view>
</view>
<view slot="foot" class="card-footer-section">
<view class="status-container">
<u-tag :text="item.state === '1' ? '已提交' : '未提交'"
:type="item.state === '1' ? 'success' : 'info'" />
<u-rate
:value="item.score / 20"
disabled
active-color="#ff9900"
size="28"
margin="0 0 0 20rpx"
/>
<view class="score">总分: {{ item.score || '0' }}</view>
</view>
<u-button
size="small"
type="primary"
@click="handleView(item)"
class="action-btn"
>
编辑/查看
</u-button>
</view>
</u-card>
<u-empty v-if="tableData.length === 0 && !loading" mode="data" />
<u-loadmore
v-if="tableData.length > 0"
:status="loadStatus"
:load-text="loadText"
/>
</scroll-view>
<view class="pagination-container" v-if="pagination.total > pagination.size">
<u-pagination
v-model="pagination.current"
:itemsPerPage="pagination.size"
:total="pagination.total"
:showTotal="true"
@change="handlePageChange"
/>
</view>
</u-card>
</view>
</template>
<script>
import { ref, reactive, onMounted } from 'vue';
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app';
export default {
setup() {
// 环境变量管理API地址
const API_BASE = 'http://172.26.26.43/dev-api';
const API_URL = ${API_BASE}/wjdc/wj/listTx;
const BDR_API_URL = ${API_BASE}/wjdc/wj/getBdrList;
// 状态选项
const stateOptions = ref([
{ label: '已提交', value: 1 },
{ label: '未提交', value: 0 }
]);
// 表单验证规则
const rules = reactive({
dcWjTitle: [
{
min: 3,
message: '标题长度至少3个字符',
trigger: 'blur'
}
],
dcId: [
{
required: true,
message: '请选择被测评人',
trigger: 'change'
}
]
});
// 被测评人相关数据
const bdrOptions = ref([]);
const bdrLoading = ref(false);
// 表单数据
const formData = reactive({
dcWjTitle: '',
dcId: [],
dcDept: '',
state: null
});
// 表格数据
const tableData = ref([]);
const loading = ref(false);
const refreshing = ref(false);
const searchKeyword = ref('');
// 分页配置
const pagination = reactive({
current: 1,
size: 10,
total: 0
});
// 加载更多状态
const loadStatus = ref('loadmore');
const loadText = reactive({
loadmore: '上拉加载更多',
loading: '正在加载...',
nomore: '没有更多了'
});
// 表单引用
const indexForm = ref(null);
// 获取认证令牌
const getAuthToken = () => {
const token = uni.getStorageSync('token');
if (!token) {
uni.showToast({ title: '请先登录', icon: 'none' });
uni.navigateTo({ url: '/pages/login/login' });
return null;
}
return token;
};
// 获取被测评人列表
const fetchBdrList = async () => {
const token = getAuthToken();
if (!token) return;
bdrLoading.value = true;
try {
const [err, res] = await uni.request({
url: BDR_API_URL,
method: 'GET',
header: { 'Authorization': Bearer ${token} }
});
if (err) throw new Error(err.errMsg || '请求失败');
const data = res.data;
if (data?.code === 200) {
bdrOptions.value = data.data.map(item => ({
value: item.dcId,
label: item.dcName
}));
} else {
throw new Error(data?.msg || '返回数据格式不正确');
}
} catch (error) {
uni.showToast({ title: 获取失败: ${error.message}, icon: 'none' });
} finally {
bdrLoading.value = false;
}
};
// 获取问卷数据
const fetchData = async () => {
const token = getAuthToken();
if (!token) return;
loading.value = true;
loadStatus.value = 'loading';
try {
const params = {
pageNum: pagination.current,
pageSize: pagination.size,
...formData,
keyword: searchKeyword.value,
dcId: formData.dcId.join(',')
};
const [err, res] = await uni.request({
url: API_URL,
method: 'GET',
data: params,
header: {
'Content-Type': 'application/json',
'Authorization': Bearer ${token}
}
});
if (err) throw new Error(err.errMsg || '请求失败');
const data = res.data;
if (data?.code === 200) {
tableData.value = data.rows || [];
pagination.total = data.total || 0;
// 更新加载状态
loadStatus.value = pagination.current * pagination.size < pagination.total
? 'loadmore'
: 'nomore';
if (tableData.value.length === 0) {
uni.showToast({ title: '没有找到匹配的数据', icon: 'none' });
}
} else {
throw new Error(data?.msg || '未知错误');
}
} catch (error) {
uni.showToast({ title: 请求失败: ${error.message}, icon: 'none' });
tableData.value = [];
pagination.total = 0;
loadStatus.value = 'loadmore';
} finally {
loading.value = false;
refreshing.value = false;
}
};
// 下拉刷新
const onRefresh = async () => {
refreshing.value = true;
pagination.current = 1;
await fetchData();
};
// 处理被测评人选择
const handleBdrSelect = (selected) => {
formData.dcId = selected.map(item => item.value);
};
// 搜索按钮处理函数
const handleSearch = async () => {
try {
// 表单验证
await indexForm.value.validate();
if (formData.dcId.length > 1) {
uni.showToast({
title: '当前只能搜索一个被测人员',
icon: 'none',
duration: 3000
});
return;
}
pagination.current = 1;
fetchData();
} catch (e) {
console.log('验证失败', e);
}
};
// 关键词搜索
const handleKeywordSearch = () => {
pagination.current = 1;
fetchData();
};
// 重置按钮处理函数
const handleReset = () => {
formData.dcWjTitle = '';
formData.dcId = [];
formData.dcDept = '';
formData.state = null;
searchKeyword.value = '';
pagination.current = 1;
fetchData();
};
// 编辑/查看
const handleView = (row) => {
uni.navigateTo({
url: /pages/operation/operation?id=${row.dcWjId}
});
};
// 页码改变
const handlePageChange = (page) => {
pagination.current = page;
fetchData();
};
// 初始化
onLoad(() => {
fetchBdrList();
fetchData();
});
// 添加下拉刷新生命周期
onPullDownRefresh(() => {
onRefresh().finally(() => {
uni.stopPullDownRefresh();
});
});
return {
formData,
bdrOptions,
stateOptions,
rules,
tableData,
loading,
pagination,
indexForm,
searchKeyword,
refreshing,
loadStatus,
loadText,
handleBdrSelect,
handleSearch,
handleReset,
handleView,
handlePageChange,
fetchData,
onRefresh,
handleKeywordSearch
};
}
};
</script>
<style scoped>
.detail-container {
padding: 20rpx;
background-color: #f8f8f8;
min-height: 100vh;
}
.mobile-form .u-form-item {
margin-bottom: 32rpx;
}
.button-group {
display: flex;
justify-content: space-between;
margin-top: 24rpx;
}
.button-group .action-button {
flex: 1;
margin: 0 12rpx;
height: 80rpx;
border-radius: 12rpx;
font-size: 32rpx;
}
.card-header {
display: flex;
align-items: center;
padding: 20rpx 0;
}
.u-body-item {
padding: 16rpx 0;
}
.card-row {
display: flex;
margin-bottom: 16rpx;
font-size: 30rpx;
line-height: 1.6;
}
.card-label {
color: #666;
min-width: 150rpx;
font-weight: bold;
}
.card-value {
color: #333;
flex: 1;
}
.card-footer-section {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20rpx;
border-top: 1rpx solid #f0f2f5;
}
.status-container {
display: flex;
align-items: center;
}
.status-container .score {
margin-left: 24rpx;
font-size: 30rpx;
color: #e6a23c;
font-weight: 500;
}
.action-btn {
min-width: 180rpx;
}
.pagination-container {
margin-top: 40rpx;
display: flex;
justify-content: center;
}
/* 响应式调整 */
@media (max-width: 768px) {
.button-group {
flex-direction: column;
}
.button-group .action-button {
margin: 12rpx 0;
width: 100%;
}
.card-footer-section {
flex-direction: column;
align-items: flex-start;
}
.status-container {
margin-bottom: 20rpx;
flex-wrap: wrap;
}
.action-btn {
width: 100%;
}
}
</style>
登录成功后页面显示空白,显示请求失败

<script lang="ts" setup>
import { ref, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { useRouter } from 'vue-router'
import request from '@/utils/request'
import type { ResultModel } from '@/api/model/model'
// 类型定义
interface PasswordForm {
oldPassword: string
newPassword: string
confirmPassword: string
}
interface LoginUser {
id: number
username: string // 增加用户名字段
name: string
role: string
}
// API调用(修改参数结构)
const updatePasswordApi = (data: { username: string; password: string; newPassword: string }) =>
request.put<any, ResultModel>('/emps/update', data)
const router = useRouter()
const loginName = ref('管理员')
const loginRole = ref('管理员')
const username = ref('') // 存储用户名
const showPasswordDialog = ref(false)
const passwordForm = ref({
oldPassword: '',
newPassword: '',
confirmPassword: '',
})
const passwordFormRef = ref<FormInstance>()
const loading = ref(false)
// 密码验证规则
const passwordRules: FormRules = {
oldPassword: [
{ required: true, message: '请输入旧密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度6-20个字符', trigger: 'blur' },
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度6-20个字符', trigger: 'blur' },
{
validator: (_, value, callback) => {
if (value === passwordForm.value.oldPassword) {
callback(new Error('新密码不能与旧密码相同'))
} else {
callback()
}
},
trigger: 'blur',
},
],
confirmPassword: [
{ required: true, message: '请确认新密码', trigger: 'blur' },
{
validator: (_, value, callback) => {
if (value !== passwordForm.value.newPassword) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
},
trigger: 'blur',
},
],
}
// 初始化用户信息(增加用户名获取)
onMounted(() => {
const token = localStorage.getItem('token')
if (token) {
try {
const loginUser: LoginUser = JSON.parse(token)
loginName.value = loginUser.name || '管理员'
loginRole.value = loginUser.role || '管理员'
username.value = loginUser.username || '' // 获取用户名
} catch (e) {
console.error('解析token失败:', e)
}
}
})
// 打开修改密码对话框(保持不变)
const openPasswordDialog = () => {
showPasswordDialog.value = true
passwordForm.value = { oldPassword: '', newPassword: '', confirmPassword: '' }
nextTick(() => {
if (passwordFormRef.value) {
passwordFormRef.value.resetFields()
}
})
}
// 提交修改密码(调整跳转逻辑)
const submitPassword = async () => {
if (!passwordFormRef.value) return
try {
await passwordFormRef.value.validate()
loading.value = true
const response = await updatePasswordApi({
username: username.value,
password: passwordForm.value.oldPassword,
newPassword: passwordForm.value.newPassword,
})
if (response.code === 1) {
ElMessage.success('密码修改成功,请重新登录')
showPasswordDialog.value = false
passwordFormRef.value.resetFields()
// 直接跳转到登录页面,不显示退出确认
localStorage.removeItem('token')
router.push('/login')
} else {
ElMessage.error(response.msg || '密码修改失败')
}
} catch (error) {
console.error('修改密码失败:', error)
ElMessage.error('请求失败,请稍后重试')
} finally {
loading.value = false
}
}
// 修改退出登录方法(保持原有的确认提示)
const logout = () => {
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
center: true,
})
.then(() => {
localStorage.removeItem('token')
router.push('/login')
ElMessage.success('退出登录成功')
})
.catch(() => {})
}
</script>
<template>
<el-container class="layout-container">
<el-header class="header">
AI教学管理系统
<el-avatar class="user-avatar" :size="36">
{{ loginName.charAt(0) }}
</el-avatar>
{{ loginName }}
{{ loginRole }}
<el-tooltip content="修改密码" placement="bottom">
<el-button class="tool-btn" type="text" @click="openPasswordDialog">
<el-icon class="icon"><EditPen /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="退出登录" placement="bottom">
<el-button class="tool-btn" type="text" @click="logout">
<el-icon class="icon"><SwitchButton /></el-icon>
</el-button>
</el-tooltip>
</el-header>
<el-container class="main-container">
<el-aside class="aside">
<el-scrollbar class="menu-scrollbar">
<el-menu
router
default-active="/index"
class="side-menu"
background-color="#2c3e50"
text-color="#ecf0f1"
active-text-color="#f39c12"
>
<el-menu-item index="/index">
<el-icon><Menu /></el-icon>
首页
</el-menu-item>
<el-sub-menu index="2" class="menu-group">
<template #title>
<el-icon><Document /></el-icon>
班级学员管理
</template>
<el-menu-item index="/stu">
<el-icon><UserFilled /></el-icon>
学员管理
</el-menu-item>
<el-menu-item index="/clazz">
<el-icon><HomeFilled /></el-icon>
班级管理
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="3" class="menu-group">
<template #title>
<el-icon><Avatar /></el-icon>
部门管理
</template>
<el-menu-item index="/emp">
<el-icon><Avatar /></el-icon>
员工管理
</el-menu-item>
<el-menu-item index="/dept">
<el-icon><HelpFilled /></el-icon>
部门管理
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="4" class="menu-group">
<template #title>
<el-icon></el-icon>
数据统计管理
</template>
<el-menu-item index="/report/emp">
<el-icon><InfoFilled /></el-icon>
员工信息统计
</el-menu-item>
<el-menu-item index="/report/stu">
<el-icon><Share /></el-icon>
学员信息统计
</el-menu-item>
<el-menu-item index="/log">
<el-icon><Clock /></el-icon>
日志信息统计
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</el-aside>
<el-main class="main-content">
<router-view></router-view>
</el-main>
</el-container>
</el-container>
<el-dialog
v-model="showPasswordDialog"
title="修改密码"
width="500px"
center
:close-on-click-modal="false"
>
<el-form
ref="passwordFormRef"
:model="passwordForm"
:rules="passwordRules"
label-width="100px"
label-position="right"
>
<el-form-item label="旧密码" prop="oldPassword">
<el-input
v-model="passwordForm.oldPassword"
type="password"
placeholder="请输入当前密码"
show-password
clearable
/>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input
v-model="passwordForm.newPassword"
type="password"
placeholder="6-20位字符"
show-password
clearable
/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="passwordForm.confirmPassword"
type="password"
placeholder="请再次输入新密码"
show-password
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showPasswordDialog = false">取消</el-button>
<el-button type="primary" @click="submitPassword" :loading="loading">
确认修改
</el-button>
</template>
</el-dialog>
</template>
<style scoped>
/* 布局基础样式 */
.common-layout {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f7fa;
}
.layout-container {
height: 100%;
display: flex;
flex-direction: column;
}
/* 头部样式 - 优化版 */
.header {
background: linear-gradient(135deg, #2c3e50 0%, #1a2530 100%);
color: white;
height: 64px;
padding: 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
position: relative;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
padding: 0 30px;
max-width: 1600px;
margin: 0 auto;
width: 100%;
}
.logo-section {
display: flex;
align-items: center;
gap: 15px;
}
.logo {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #3498db, #8e44ad);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.title {
font-size: 1.8rem;
font-weight: 700;
letter-spacing: 1px;
background: linear-gradient(to right, #f39c12, #f1c40f);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.user-section {
display: flex;
align-items: center;
gap: 20px;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
padding: 5px 15px;
border-radius: 30px;
background: rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.user-info:hover {
background: rgba(255, 255, 255, 0.15);
}
.user-avatar {
background: linear-gradient(135deg, #3498db, #8e44ad);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
font-weight: 600;
font-size: 15px;
letter-spacing: 0.5px;
}
.user-role {
font-size: 12px;
color: #bdc3c7;
margin-top: 2px;
}
.header-tools {
display: flex;
gap: 8px;
}
.tool-btn {
color: #ecf0f1;
font-size: 18px;
padding: 8px;
transition: all 0.3s ease;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.tool-btn:hover {
background-color: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
}
.tool-btn .icon {
transition: transform 0.3s ease;
}
.tool-btn:hover .icon {
transform: scale(1.2);
}
/* 侧边栏样式 */
.aside {
background-color: #2c3e50;
color: #ecf0f1;
height: 100%;
width: 240px !important;
transition: width 0.3s ease;
border-right: 1px solid #34495e;
}
.menu-scrollbar {
height: calc(100vh - 64px);
}
.menu-scrollbar .el-scrollbar__bar.is-vertical {
width: 6px;
right: 2px;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.1);
}
.menu-scrollbar .el-scrollbar__thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 4px;
transition: background-color 0.3s ease;
}
.menu-scrollbar .el-scrollbar__thumb:hover {
background-color: rgba(255, 255, 255, 0.5);
}
.side-menu {
background-color: transparent;
border-right: none;
padding: 15px 0;
}
.side-menu .el-menu-item,
.side-menu .el-sub-menu .el-sub-menu__title {
margin: 6px 10px;
padding: 12px 20px;
font-size: 14px;
color: #ecf0f1;
border-radius: 6px;
transition: all 0.3s ease;
height: 48px;
display: flex;
align-items: center;
}
.side-menu .el-menu-item:hover,
.side-menu .el-sub-menu .el-sub-menu__title:hover {
background-color: #34495e;
transform: translateX(5px);
}
.side-menu .el-menu-item.is-active {
background: linear-gradient(90deg, rgba(52, 152, 219, 0.3), transparent);
color: #f39c12 !important;
font-weight: bold;
border-left: 3px solid #f39c12;
}
.menu-group .el-menu-item {
font-size: 13px;
padding-left: 50px !important;
color: #dcdcdc;
transition: all 0.3s ease;
height: 42px;
}
.menu-group .el-menu-item:hover {
background-color: #3a5267;
transform: translateX(5px);
}
.menu-group .el-menu-item.is-active {
background-color: #3a5267;
color: #f1c40f;
font-weight: 600;
}
/* 主内容区样式 */
.main-content {
background-color: #f0f2f5;
padding: 20px;
flex: 1;
overflow-y: auto;
border-radius: 8px 8px 0 0;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
/* 对话框样式 */
.dialog-footer {
display: flex;
justify-content: center;
padding: 10px 20px 0;
}
/* 响应式设计 */
@media (max-width: 992px) {
.aside {
width: 200px !important;
}
.header-content {
padding: 0 20px;
}
.title {
font-size: 1.5rem;
}
.user-info .user-name {
display: none;
}
}
@media (max-width: 768px) {
.aside {
width: 64px !important;
}
.logo-section .title,
.user-info .user-details,
.el-sub-menu .el-sub-menu__title span,
.el-menu-item span {
display: none;
}
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
justify-content: center;
padding: 0 !important;
}
.el-icon {
margin-right: 0 !important;
font-size: 20px;
}
.menu-group .el-menu-item {
padding-left: 20px !important;
justify-content: flex-start;
}
}
</style>
优化一下页面自适应大小,页面缩小之后右上角的退出登录会被挡住

<template>
<view class="detail-container">
<view class="index-card">
<u-form :model="formData" ref="indexForm" class="mobile-form">
<u-form-item label="问卷标题:" prop="dcWjTitle" label-width="150rpx">
<u-input
v-model="formData.dcWjTitle"
placeholder="请输入问卷标题"
clearable
border="bottom"
prefixIcon="file-text"
/>
</u-form-item>
<u-form-item label="被测评人:" prop="dcId" label-width="150rpx">
<u-select
v-model="formData.dcId"
:list="bdrOptions"
multiple
value-name="dcId"
label-name="dcName"
@confirm="handleBdrSelect"
placeholder="请选择被测评人"
></u-select>
</u-form-item>
<u-form-item label="人员部门:" prop="dcDept" label-width="150rpx">
<u-input
v-model="formData.dcDept"
placeholder="请输入人员部门"
clearable
border="bottom"
prefixIcon="home"
/>
</u-form-item>
<u-form-item label="人员状态:" prop="state" label-width="150rpx">
<u-select
v-model="formData.state"
:list="stateOptions"
value-name="value"
label-name="label"
placeholder="请选择提交状态"
></u-select>
</u-form-item>
<view class="button-group">
<u-button
type="primary"
@click="handleSearch"
class="action-button"
icon="search"
>
搜索
</u-button>
<u-button
@click="handleReset"
class="action-button"
icon="reload"
>
重置
</u-button>
</view>
</u-form>
</view>
<view class="data-card">
<view class="card-header">
<u-button
type="primary"
size="small"
icon="reload"
@click="fetchData"
>
刷新数据
</u-button>
</view>
<view class="card-container">
<u-loading-page :loading="loading" loadingText="加载中..." />
<view
v-for="(item, index) in tableData"
:key="item.dcWjId"
class="data-card-item"
>
<view class="card-header-section">
<view class="card-title">{{ item.dcWjTitle }}</view>
</view>
<view class="card-body-section">
<view class="card-row">
<text class="card-label">被测评人:</text>
<text class="card-value">{{ item.dcName }}</text>
</view>
<view class="card-row">
<text class="card-label">部门:</text>
<text class="card-value">{{ item.dcDept }}</text>
</view>
<view class="card-row">
<text class="card-label">创建时间:</text>
<text class="card-value">{{ item.createTime }}</text>
</view>
<view class="card-row">
<text class="card-label">提交时间:</text>
<text class="card-value">{{ item.updateTime || '-' }}</text>
</view>
</view>
<view class="card-footer-section">
<view class="status-container">
<u-tag :text="item.state === '1' ? '已提交' : '未提交'"
:type="item.state === '1' ? 'success' : 'info'" />
<view class="score">总分: {{ item.score || '0' }}</view>
</view>
<u-button
size="small"
type="primary"
@click="handleView(item)"
class="action-btn"
>
编辑/查看
</u-button>
</view>
</view>
<u-empty v-if="tableData.length === 0" mode="data" />
</view>
<view class="pagination-container">
<u-pagination
v-model="pagination.current"
:itemsPerPage="pagination.size"
:total="pagination.total"
:showTotal="true"
@change="handlePageChange"
/>
</view>
</view>
</view>
</template>
<script>
import { ref, reactive, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
export default {
setup() {
// 环境变量管理API地址 - 使用uni-app兼容方式
const API_BASE = 'http://172.26.26.43/dev-api'; // 替换为实际API地址
const API_URL = ${API_BASE}/wjdc/wj/listTx;
const BDR_API_URL = ${API_BASE}/wjdc/wj/getBdrList;
// 状态选项
const stateOptions = ref([
{ label: '已提交', value: 1 },
{ label: '未提交', value: 0 }
]);
// 被测评人相关数据
const bdrOptions = ref([]); // 被测评人选项列表
const bdrLoading = ref(false); // 加载状态
const bdrCache = ref([]); // 缓存所有被测评人数据
// 表单数据
const formData = reactive({
dcWjTitle: '',
dcId: [],
dcDept: '',
state: null
});
// 表格数据
const tableData = ref([]);
const loading = ref(false);
// 分页配置
const pagination = reactive({
current: 1,
size: 10,
total: 0
});
// 表单引用
const indexForm = ref(null);
// 处理被测评人选择
const handleBdrSelect = (selected) => {
formData.dcId = selected.map(item => item.value);
};
// 获取被测评人列表
const fetchBdrList = async () => {
const token = getAuthToken();
if (!token) return;
bdrLoading.value = true;
try {
const [err, res] = await uni.request({
url: BDR_API_URL,
method: 'GET',
header: {
'Authorization': Bearer ${token}
}
});
if (err) {
throw new Error(err.errMsg || '请求失败');
}
const data = res.data;
if (data && data.code === 200) {
bdrCache.value = data.data || [];
bdrOptions.value = bdrCache.value.map(item => ({
value: item.dcId,
label: item.dcName
}));
} else {
const msg = data?.msg || '返回数据格式不正确';
uni.showToast({
title: '获取被测评人列表失败: ' + msg,
icon: 'none'
});
}
} catch (error) {
console.error('获取被测评人列表失败:', error);
uni.showToast({
title: '获取被测评人列表失败: ' + error.message,
icon: 'none'
});
} finally {
bdrLoading.value = false;
}
};
const token = getAuthToken();
if (!token) return;
bdrLoading.value = true;
try {
const response = await uni.request({
url: BDR_API_URL,
method: 'GET',
params: {
pageNum: pagination.current,
pageSize: pagination.size,
...formData,
dcId: formData.dcId.join(',')
},
header: {
'Authorization': Bearer ${token}
}
});
const data = response[1].data;
if (data && data.code === 200) {
bdrCache.value = data.data || [];
bdrOptions.value = bdrCache.value.map(item => ({
value: item.dcId,
label: item.dcName
}));
} else {
const msg = data?.msg || '返回数据格式不正确';
uni.showToast({
title: '获取被测评人列表失败: ' + msg,
icon: 'none'
});
}
} catch (error) {
console.error('获取被测评人列表失败:', error);
uni.showToast({
title: '获取被测评人列表失败',
icon: 'none'
});
} finally {
bdrLoading.value = false;
}
};
// 获取认证令牌
const getAuthToken = () => {
const token = uni.getStorageSync('token');
if (!token) {
uni.showToast({
title: '请先登录',
icon: 'none'
});
uni.navigateTo({
url: '/pages/login/login'
});
return null;
}
return token;
};
// 搜索按钮处理函数
const handleSearch = () => {
// 检查被测评人选择数量
if (formData.dcId.length > 1) {
uni.showToast({
title: '当前只能搜索一个被测人员',
icon: 'none',
duration: 3000
});
return;
}
pagination.current = 1;
fetchData();
};
// 重置按钮处理函数
const handleReset = () => {
formData.dcWjTitle = '';
formData.dcId = [];
formData.dcDept = '';
formData.state = null;
handleSearch();
};
// 编辑/查看
const handleView = (row) => {
uni.navigateTo({
url: /pages/operation/operation?id=${row.dcWjId}
});
};
// 页码改变
const handlePageChange = (page) => {
pagination.current = page;
fetchData();
};
// 获取数据
const fetchData = async () => {
const token = getAuthToken();
if (!token) return;
loading.value = true;
try {
const params = {
pageNum: pagination.current,
pageSize: pagination.size,
...formData,
dcId: formData.dcId.join(',')
};
const [err, res] = await uni.request({
url: API_URL,
method: 'GET',
data: params,
header: {
'Content-Type': 'application/json',
'Authorization': Bearer ${token}
}
});
if (err) {
throw new Error(err.errMsg || '请求失败');
}
const data = res.data;
if (data && data.code === 200) {
tableData.value = data.rows || [];
pagination.total = data.total || 0;
if (tableData.value.length === 0) {
uni.showToast({
title: '没有找到匹配的数据',
icon: 'none'
});
}
} else {
const errorMsg = data?.msg || '未知错误';
console.error('API返回错误:', errorMsg);
uni.showToast({
title: 请求失败: ${errorMsg},
icon: 'none'
});
tableData.value = [];
pagination.total = 0;
}
} catch (error) {
// ... 错误处理 ...
} finally {
loading.value = false;
}
};
const token = getAuthToken();
if (!token) return;
loading.value = true;
try {
const params = {
pageNum: pagination.current,
pageSize: pagination.size,
...formData,
dcId: formData.dcId.join(',')
};
const response = await uni.request({
url: API_URL,
method: 'GET',
data: params,
header: {
'Content-Type': 'application/json',
'Authorization': Bearer ${token}
}
});
const data = response[1].data;
if (data && data.code === 200) {
tableData.value = data.rows || [];
pagination.total = data.total || 0;
if (tableData.value.length === 0) {
uni.showToast({
title: '没有找到匹配的数据',
icon: 'none'
});
}
} else {
const errorMsg = data?.msg || '未知错误';
console.error('API返回错误:', errorMsg);
uni.showToast({
title: 请求失败: ${errorMsg},
icon: 'none'
});
tableData.value = [];
pagination.total = 0;
}
} catch (error) {
// 安全地访问错误属性
const statusCode = error.statusCode || error.errMsg?.match(/status code (\d+)/)?.[1];
if (statusCode === '401') {
uni.showToast({
title: '认证过期,请重新登录',
icon: 'none'
});
uni.removeStorageSync('token');
uni.navigateTo({
url: '/pages/login/login'
});
return;
}
console.error('获取数据失败:', error);
// 安全地获取错误信息
let errorMsg = '网络请求失败';
if (error.errMsg) {
errorMsg = error.errMsg;
} else if (error.message) {
errorMsg = error.message;
} else if (typeof error === 'string') {
errorMsg = error;
}
uni.showToast({
title: 请求失败: ${errorMsg},
icon: 'none'
});
tableData.value = [];
pagination.total = 0;
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchBdrList();
fetchData();
});
return {
formData,
bdrOptions,
stateOptions,
tableData,
loading,
pagination,
indexForm,
handleBdrSelect,
handleSearch,
handleReset,
handleView,
handlePageChange,
fetchData
};
}
};
</script>
<style>
.detail-container {
padding: 20rpx;
background-color: #f8f8f8;
min-height: 100vh;
}
.index-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.05);
}
.mobile-form .u-form-item {
margin-bottom: 32rpx;
}
.mobile-form .u-input,
.mobile-form .u-select {
background: #f8f8f8;
border-radius: 12rpx;
padding: 20rpx;
margin-top: 12rpx;
}
.button-group {
display: flex;
justify-content: space-between;
margin-top: 24rpx;
}
.button-group .action-button {
flex: 1;
margin: 0 12rpx;
height: 80rpx;
border-radius: 12rpx;
font-size: 32rpx;
}
.data-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.05);
}
.data-card .card-header {
margin-bottom: 24rpx;
display: flex;
justify-content: flex-end;
}
.card-container .data-card-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
border: 1rpx solid #eee;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.card-container .card-header-section {
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f2f5;
margin-bottom: 20rpx;
}
.card-container .card-header-section .card-title {
font-size: 34rpx;
font-weight: bold;
color: #333;
}
.card-container .card-body-section {
margin-bottom: 20rpx;
}
.card-container .card-row {
display: flex;
margin-bottom: 16rpx;
font-size: 30rpx;
line-height: 1.6;
}
.card-container .card-label {
color: #666;
min-width: 150rpx;
}
.card-container .card-value {
color: #333;
flex: 1;
}
.card-container .card-footer-section {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20rpx;
border-top: 1rpx solid #f0f2f5;
}
.card-container .status-container {
display: flex;
align-items: center;
}
.card-container .status-container .score {
margin-left: 24rpx;
font-size: 30rpx;
color: #e6a23c;
font-weight: 500;
}
.card-container .action-btn {
min-width: 180rpx;
}
.pagination-container {
margin-top: 40rpx;
display: flex;
justify-content: center;
}
/* 响应式调整 - 移动端 */
@media (max-width: 768px) {
.button-group {
flex-direction: column;
}
.button-group .action-button {
margin: 12rpx 0;
width: 100%;
}
.card-container .card-footer-section {
flex-direction: column;
align-items: flex-start;
}
.card-container .status-container {
margin-bottom: 20rpx;
}
.card-container .action-btn {
width: 100%;
}
}
</style>
这个代码怎么修改

<script lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
import request from '@/utils/request'
import type { ResultModel } from '@/api/stu'
// 定义登录请求数据类型
interface LoginData {
username: string
password: string
}
//修改密码
export const updatePasswordApi = (data: LoginData) =>
request.put<any, ResultModel>('/emps/update', data)
//修改员工信息
let router = useRouter()
const loginName = ref('')
//定义钩子函数, 获取登录用户名
onMounted(() => {
//获取登录用户名
const token = localStorage.getItem('token') || '{}'
let loginUser = JSON.parse(token)
console.log(loginUser)
if (loginUser) {
loginName.value = loginUser.name
}
})
const logout = () => {
//弹出确认框, 如果确认, 则退出登录, 跳转到登录页面
ElMessageBox.confirm('确认退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
//确认, 则清空登录信息
ElMessage.success('退出登录成功')
localStorage.removeItem('loginUser')
router.push('/login') //跳转到登录页面
})
}
</script>
<template>
<el-container class="layout-container">
<el-header class="header">
AI教学管理系统
<el-icon><EditPen /></el-icon>
修改密码
<el-icon><SwitchButton /></el-icon>
退出登录 【{{ loginName }}】
</el-header>
<el-container class="main-container">
<el-aside class="aside">
<el-scrollbar class="menu-scrollbar">
<el-menu
router
default-active="1"
class="side-menu"
background-color="#2c3e50"
text-color="#ecf0f1"
active-text-color="#f39c12"
>
<el-menu-item index="/index">
<el-icon><Menu /></el-icon>
首页
</el-menu-item>
<el-sub-menu index="2" class="menu-group">
<template #title>
<el-icon><Document /></el-icon>
班级学员管理
</template>
<el-menu-item index="/stu"
><el-icon><UserFilled /></el-icon>学员管理</el-menu-item
>
<el-menu-item index="/clazz"
><el-icon><HomeFilled /></el-icon>班级管理</el-menu-item
>
</el-sub-menu>
<el-sub-menu index="3" class="menu-group">
<template #title>
<el-icon><Avatar /></el-icon>
部门管理
</template>
<el-menu-item index="/emp"
><el-icon><Avatar /></el-icon>员工管理</el-menu-item
>
<el-menu-item index="/dept"
><el-icon><HelpFilled /></el-icon>部门管理</el-menu-item
>
</el-sub-menu>
<el-sub-menu index="4" class="menu-group">
<template #title>
<el-icon></el-icon>
数据统计管理
</template>
<el-menu-item index="/report/emp"
><el-icon><InfoFilled /></el-icon>员工信息统计</el-menu-item
>
<el-menu-item index="/report/stu"
><el-icon><Share /></el-icon>学员信息统计</el-menu-item
>
<el-menu-item index="/log"
><el-icon><clock /></el-icon>日志信息统计</el-menu-item
>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</el-aside>
<el-main class="main-content">
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<style scoped>
/* 布局基础样式 */
.common-layout {
height: 100vh;
display: flex;
flex-direction: column;
}
.layout-container {
height: 100%;
display: flex;
flex-direction: column;
}
/* 头部样式 */
.header {
background-color: #2c3e50;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.title {
font-size: 1.5rem;
font-weight: bold;
}
.tools a {
color: #ecf0f1;
text-decoration: none;
margin-left: 20px;
display: inline-flex;
align-items: center;
transition: background-color 0.3s ease;
border-radius: 4px;
padding: 6px 10px;
}
.tools a:hover {
background-color: #34495e;
}
/* 侧边栏样式 */
.aside {
background-color: #2c3e50;
color: #ecf0f1;
height: 100%;
padding-bottom: 20px;
}
/* 自定义滚动条样式 */
.menu-scrollbar {
height: calc(100vh - 60px); /* 减去 header 高度 */
}
.menu-scrollbar .el-scrollbar__bar.is-vertical {
width: 6px;
right: 2px;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.1);
}
.menu-scrollbar .el-scrollbar__thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 4px;
transition: background-color 0.3s ease;
}
.menu-scrollbar .el-scrollbar__thumb:hover {
background-color: rgba(255, 255, 255, 0.5);
}
/* 菜单样式 */
.side-menu {
background-color: transparent;
border-right: none;
padding: 10px 0;
}
/* 菜单项通用样式 */
.side-menu .el-menu-item,
.side-menu .el-sub-menu .el-sub-menu__title {
margin: 6px 0;
padding: 10px 20px;
font-size: 14px;
color: #ecf0f1;
border-radius: 6px;
transition: all 0.3s ease;
}
/* 悬停样式 */
.side-menu .el-menu-item:hover,
.side-menu .el-sub-menu .el-sub-menu__title:hover {
background-color: #34495e;
}
/* 激活项样式 */
.side-menu .el-menu-item.is-active {
background-color: #34495e;
color: #f39c12 !important;
font-weight: bold;
}
/* 子菜单项样式 */
.menu-group .el-menu-item {
font-size: 13px;
padding-left: 40px !important;
color: #dcdcdc;
transition: all 0.3s ease;
}
.menu-group .el-menu-item:hover {
background-color: #3a5267;
}
.menu-group .el-menu-item.is-active {
background-color: #3a5267;
color: #f1c40f;
}
/* 主内容区样式 */
.main-content {
background-color: #f9f9f9;
padding: 20px;
flex: 1;
overflow-y: auto;
}
/* 响应式设计 */
@media (max-width: 768px) {
.tools a {
margin-left: 10px;
font-size: 0.9rem;
padding: 4px 8px;
}
.title {
font-size: 1.2rem;
}
.menu-group .el-menu-item {
font-size: 12px;
padding-left: 25px !important;
}
}
</style>
优化一下代码逻辑,以及头部效果 ,有个修改密码的api完善一下修改密码功能

<template>
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" label-position="top">
<el-form-item label="用户名" prop="username">
<el-input v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="loginForm.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="login">登录</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { login } from '@/api';
export default {
name: 'LoginPage',
data() {
return {
loginForm: {
username: '',
password: ''
},
loginRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
]
}
};
},
methods: {
login() {
this.$refs.loginForm.validate(valid => {
if (valid) {
// 登录逻辑,可以发送请求到后端进行验证
const { username, password } = this.loginForm; // 修改这里
console.log(username, password);
login(username, password)
.then(response => {
// 登录成功,处理响应
console.log(response.data);
// 进行其他操作,例如跳转到其他页面
this.$router.push('/');
})
.catch(error => {
// 登录失败,处理错误
console.error(error);
// 进行错误提示或其他操作
});
console.log('登录成功');
// 登录成功后跳转到首页
} else {
console.log('表单验证失败');
}
});
}
}
};
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
</style> 密码验证成功后路由没有正常跳转到首页

如下述代码1,是一个弹窗的代码页面(如图1所示),调整一下左侧的"SQL查询",当分辨率高变形的有点厉害了,请参照代码2中的样式所示,请优化"SQL查询"的样式,并完整的写出修改后的代码(不改变代码1中的功能,即"sql查询"框中能够输入sql语句不变)
### 代码1:src\views\handle\dataset\add\AddView.vue
vue
<template>
<el-container class="layout-container">
<el-aside width="40%" class="config-aside">
<el-icon><setting /></el-icon> SQL数据集配置
<el-form
:model="form"
:rules="rules"
label-position="top"
class="config-form"
ref="formRef"
>
<el-form-item v-if="props.id===''" label="数据集名称" prop="name" class="form-item-card inline-form-item">
<el-input
v-model="form.name"
placeholder="例如: 用户行为分析数据集"
size="large"
:prefix-icon="Document"
/>
</el-form-item>
<el-form-item label="SQL查询" prop="sql" class="form-item-card sql-editor-item">
<VAceEditor
v-model:value="form.sql"
lang="sql"
theme="github"
style="height: 300px; width: 100%"
:options="{
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
highlightActiveLine: true,
showLineNumbers: true,
tabSize: 2,
}"
/>
<el-tag type="info" size="small">
<el-icon><info-filled /></el-icon>
提示: 请确保SQL语法正确且符合数据源规范
</el-tag>
</el-form-item>
</el-form>
</el-aside>
<el-main class="preview-main">
<el-icon><data-line /></el-icon> 数据预览
<el-button type="warning" plain size="small" @click="emit('cancel')" :icon="Close">取消</el-button>
<el-button type="success" plain size="small"
:disabled="!form.sql" @click="fetchPreviewData" :icon="Refresh">执行SQL预览</el-button>
<el-button type="primary" plain size="small" @click="handleSave" :icon="Check">保存</el-button>
<zr-table :tableModule="tableModule"
/>
</el-main>
</el-container>
</template>
<script setup>
import {ref, reactive, onMounted, getCurrentInstance} from 'vue'
import { VAceEditor } from 'vue3-ace-editor'
import '@/components/CodeEdit/ace-config.js'
import {
Setting,
DataLine,
Document,
Refresh,
Check,
Close,
InfoFilled, Edit, Download
} from '@element-plus/icons-vue'
import ZrTable from "@/components/ZrTable/index.vue";
import {buildFilterSos} from "@/components/ZrTable/table.js";
import {
handleDataSetCreate,
handleDataSetGet,
handleDataSetPreviewData,
handleDataSetReconstruct
} from "@/api/handle/dataset.js";
const { proxy } = getCurrentInstance()
const props = defineProps({
sceneId:{
type: String,
default: '',
},
id:{
type: String,
default: '',
}
})
const emit = defineEmits(['saved', 'cancel'])
const form = ref({
id:props.id,
name: '',
sceneId:props.sceneId,
type:'view',
sql: '',
info:'',
})
//数据预览
const columnsData= ref([])
const queryData = ref([])
const state = reactive({
columns: columnsData, // 表格配置
query: queryData, // 查询条件配置
queryForm: {}, // 查询form表单
loading: false, // 加载状态
dataList: [], // 列表数据
pages:false,
})
const { loading, dataList, columns, pages, query, queryForm } = toRefs(state)
const formRef = ref(null)
// 预览数据
// 传给子组件的
const tableModule = ref({
callback: fetchPreviewData, // 回调,子组件中可以看到很多调用callback的,这里对应的是获取列表数据的方法
// 以下不说了,上面都给解释了
queryForm,
columns,
dataList,
loading,
pages,
query,
})
const rules = reactive({
name: [
{ required: true, message: '请输入数据集名称', trigger: 'blur' },
{ max: 50, message: '名称长度不能超过50个字符', trigger: 'blur' }
],
sql: [
{ required: true, message: '请输入SQL查询语句', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (!value || !value.trim()) {
callback(new Error('SQL不能为空'))
} else if (!value.toLowerCase().includes('select')) {
callback(new Error('SQL必须是SELECT查询语句'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
async function fetchPreviewData() {
state.loading = true
const valid = await formRef.value.validate()
if (!valid) {
proxy.$modal.msgError("校验错误,请修改好再次执行")
return
}
// 掉自己的接口,切勿复制粘贴
handleDataSetPreviewData({
sql:form.value.sql
}).then((res) => {
if (res.success) {
if(res.data.pageData.length>0){
for (let key in res.data.pageData[0]) {
columnsData.value.push({prop: key, label:key, align: 'center',show:1})
}
}
state.dataList = res.data.pageData
proxy.$modal.msgSuccess('SQL执行成功')
} else {
proxy.$modal.msgError(res.message)
}
})
state.loading = false
}
const handleSave = async () => {
const valid = await formRef.value.validate()
if (!valid) return
form.value.info=JSON.stringify({
sql:form.value.sql
})
const method=form.value.id===""?handleDataSetCreate:handleDataSetReconstruct
method(form.value).then((res) => {
if (res.success) {
emit('saved',{id: res.data })
} else {
proxy.$modal.msgError(res.message)
}
})
}
watch(() => props.id,
(newid, oldid) => {
if (newid !== oldid && newid!=="") { // 引用变化时触发
handleDataSetGet(newid).then((res) => {
if(res.success){
form.value.sql=JSON.parse(res.data.info)?.sql;
}
})
}
},
{ immediate: true } // 注意:不要用 deep: true
);
</script>
<style scoped lang="scss">
.sql-dataset-container {
height: calc(90vh - 60px);
padding: 20px;
background-color: #f5f7fa;
}
.layout-container {
height: 100%;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
overflow: hidden;
display: flex;
}
.config-aside {
background: #f9fafc;
border-right: 1px solid var(--el-border-color-light);
padding: 24px;
display: flex;
flex-direction: column;
}
.preview-main {
padding: 24px;
background: #fff;
display: flex;
flex-direction: column;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h2 {
margin: 0;
color: var(--el-text-color-primary);
font-weight: 600;
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
}
.header-left, .header-right {
display: flex;
align-items: center;
}
}
.preview-content {
flex: 1;
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.config-form {
flex: 1;
display: flex;
flex-direction: column;
:deep(.el-form-item) {
margin-bottom: 20px;
&.inline-form-item {
:deep(.el-form-item__label) {
display: inline-flex;
align-items: center;
width: auto;
margin-right: 12px;
padding-bottom: 0;
}
:deep(.el-form-item__content) {
display: inline-flex;
flex: 1;
}
}
.el-form-item__label {
font-weight: 500;
padding-bottom: 8px;
color: var(--el-text-color-regular);
font-size: 14px;
}
}
}
.form-item-card {
background: #fff;
padding: 16px;
border-radius: 8px;
border-left: 3px solid var(--el-color-primary);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.sql-editor-item {
flex: 1;
display: flex;
flex-direction: column;
:deep(.el-form-item__content) {
flex: 1;
display: flex;
flex-direction: column;
}
}
.sql-editor-container {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid var(--el-border-color-light);
border-radius: 4px;
overflow: hidden;
}
.sql-tips {
padding: 8px 12px;
background: var(--el-color-info-light-9);
border-top: 1px solid var(--el-border-color-light);
.el-tag {
width: 100%;
justify-content: flex-start;
.el-icon {
margin-right: 6px;
}
}
}
.form-actions {
margin-top: 24px;
padding-top: 16px;
display: flex;
justify-content: flex-end;
gap: 16px;
border-top: 1px dashed var(--el-border-color);
}
.refresh-btn {
:deep(.el-icon) {
margin-right: 6px;
}
}
@media (max-width: 992px) {
.layout-container {
flex-direction: column;
}
.config-aside {
width: 100% !important;
border-right: none;
border-bottom: 1px solid var(--el-border-color-light);
}
}
</style>
### 代码2:src\views\handle\dataset\add\AddView.vue
vue
<template>
<el-container class="layout-container">
<el-aside class="config-aside">
<el-icon><setting /></el-icon> SQL数据集配置
<el-form
:model="form"
:rules="rules"
label-position="top"
class="config-form"
ref="formRef"
>
<el-form-item v-if="props.id === ''" label="数据集名称" prop="name" class="form-item-card inline-form-item">
<el-input
v-model="form.name"
placeholder="例如: 用户行为分析数据集"
size="large"
:prefix-icon="Document"
/>
</el-form-item>
<el-form-item label="SQL查询" prop="sql" class="form-item-card sql-editor-item">
<VAceEditor
v-model:value="form.sql"
lang="sql"
theme="github"
style="height: 100%; width: 100%"
:options="{
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
highlightActiveLine: true,
showLineNumbers: true,
tabSize: 2,
}"
/>
<el-tag type="info" size="small">
<el-icon><info-filled /></el-icon>
提示: 请确保SQL语法正确且符合数据源规范
</el-tag>
</el-form-item>
</el-form>
</el-aside>
<el-main class="preview-main">
<el-icon><data-line /></el-icon> 数据预览
<el-button type="warning" plain size="small" @click="emit('cancel')" :icon="Close">取消</el-button>
<el-button type="success" plain size="small"
:disabled="!form.sql" @click="fetchPreviewData" :icon="Refresh">执行SQL预览</el-button>
<el-button type="primary" plain size="small" @click="handleSave" :icon="Check">保存</el-button>
<zr-table :tableModule="tableModule"
/>
</el-main>
</el-container>
</template>
<script setup>
import {ref, reactive, onMounted, getCurrentInstance, watch} from 'vue'
import { VAceEditor } from 'vue3-ace-editor'
import '@/components/CodeEdit/ace-config.js'
import {
Setting,
DataLine,
Document,
Refresh,
Check,
Close,
InfoFilled, Edit, Download
} from '@element-plus/icons-vue'
import ZrTable from "@/components/ZrTable/index.vue";
import {buildFilterSos} from "@/components/ZrTable/table.js";
import {
handleDataSetCreate,
handleDataSetGet,
handleDataSetPreviewData,
handleDataSetReconstruct
} from "@/api/handle/dataset.js";
const { proxy } = getCurrentInstance()
const props = defineProps({
sceneId:{
type: String,
default: '',
},
id:{
type: String,
default: '',
}
})
const emit = defineEmits(['saved', 'cancel'])
const form = ref({
id:props.id,
name: '',
sceneId:props.sceneId,
type:'view',
sql: '',
info:'',
})
//数据预览
const columnsData= ref([])
const queryData = ref([])
const state = reactive({
columns: columnsData, // 表格配置
query: queryData, // 查询条件配置
queryForm: {}, // 查询form表单
loading: false, // 加载状态
dataList: [], // 列表数据
pages:false,
})
const { loading, dataList, columns, pages, query, queryForm } = toRefs(state)
const formRef = ref(null)
// 预览数据
// 传给子组件的
const tableModule = ref({
callback: fetchPreviewData, // 回调,子组件中可以看到很多调用callback的,这里对应的是获取列表数据的方法
// 以下不说了,上面都给解释了
queryForm,
columns,
dataList,
loading,
pages,
query,
})
const rules = reactive({
name: [
{ required: true, message: '请输入数据集名称', trigger: 'blur' },
{ max: 50, message: '名称长度不能超过50个字符', trigger: 'blur' }
],
sql: [
{ required: true, message: '请输入SQL查询语句', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (!value || !value.trim()) {
callback(new Error('SQL不能为空'))
} else if (!value.toLowerCase().includes('select')) {
callback(new Error('SQL必须是SELECT查询语句'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
async function fetchPreviewData() {
state.loading = true
const valid = await formRef.value.validate()
if (!valid) {
proxy.$modal.msgError("校验错误,请修改好再次执行")
return
}
// 掉自己的接口,切勿复制粘贴
handleDataSetPreviewData({
sql:form.value.sql
}).then((res) => {
if (res.success) {
columnsData.value = []; // 重置列数据
if(res.data.pageData.length>0){
for (let key in res.data.pageData[0]) {
columnsData.value.push({prop: key, label:key, align: 'center',show:1})
}
}
state.dataList = res.data.pageData
proxy.$modal.msgSuccess('SQL执行成功')
} else {
proxy.$modal.msgError(res.message)
}
})
state.loading = false
}
const handleSave = async () => {
const valid = await formRef.value.validate()
if (!valid) return
form.value.info=JSON.stringify({
sql:form.value.sql
})
const method=form.value.id===""?handleDataSetCreate:handleDataSetReconstruct
method(form.value).then((res) => {
if (res.success) {
emit('saved',{id: res.data })
} else {
proxy.$modal.msgError(res.message)
}
})
}
watch(() => props.id,
(newid, oldid) => {
if (newid !== oldid && newid!=="") { // 引用变化时触发
handleDataSetGet(newid).then((res) => {
if(res.success){
form.value.sql=JSON.parse(res.data.info)?.sql;
}
})
}
},
{ immediate: true } // 注意:不要用 deep: true
);
</script>
<style scoped lang="scss">
.sql-dataset-container {
height: calc(90vh - 60px);
padding: 16px;
background-color: #f5f7fa;
}
.layout-container {
height: 100%;
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.05);
overflow: hidden;
display: flex; /* 使用flex横向布局 */
flex-direction: row; /* 横向排列 */
}
.config-aside {
background: #f9fafc;
border-right: 1px solid var(--el-border-color-light); /* 右侧边框分隔 */
padding: 18px;
display: flex;
flex-direction: column;
width: 50%; /* 左侧占50%宽度 */
min-width: 300px; /* 最小宽度限制 */
}
.preview-main {
padding: 18px;
background: #fff;
display: flex;
flex-direction: column;
flex: 1; /* 右侧占剩余宽度 */
min-width: 300px; /* 最小宽度限制 */
}
.section-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
gap: 12px;
h2 {
margin: 0;
color: var(--el-text-color-primary);
font-weight: 600;
font-size: 16px;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
}
.preview-content {
flex: 1;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 250px;
}
.config-form {
flex: 1;
display: flex;
flex-direction: column;
:deep(.el-form-item) {
margin-bottom: 16px;
}
}
.form-item-card {
background: #fff;
padding: 12px;
border-radius: 6px;
border-left: 2px solid var(--el-color-primary);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
}
.sql-editor-container {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid var(--el-border-color-light);
border-radius: 4px;
overflow: hidden;
min-height: 220px;
max-height: 400px;
}
.sql-tips {
padding: 6px 10px;
background: var(--el-color-info-light-9);
border-top: 1px solid var(--el-border-color-light);
.el-tag {
width: 100%;
justify-content: flex-start;
font-size: 11px;
padding: 4px 8px;
}
}
/* 响应式调整 */
@media (max-width: 992px) {
.layout-container {
flex-direction: column; /* 小屏幕下改为纵向布局 */
}
.config-aside,
.preview-main {
width: 100%; /* 占满宽度 */
min-width: auto; /* 取消最小宽度限制 */
}
.config-aside {
border-right: none;
border-bottom: 1px solid var(--el-border-color-light); /* 底部边框分隔 */
max-height: 50%; /* 限制最大高度 */
}
.sql-dataset-container {
padding: 12px;
height: auto;
min-height: 100vh;
}
.section-header {
flex-direction: column;
align-items: stretch;
.header-left, .header-right {
width: 100%;
}
.header-right {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: 8px;
}
}
.sql-editor-container {
max-height: 250px;
min-height: 180px;
}
}
@media (max-width: 768px) {
.preview-content {
min-height: 200px;
}
.section-header {
gap: 8px;
h2 {
font-size: 15px;
}
}
.header-right {
grid-template-columns: repeat(2, 1fr) !important;
.el-button {
width: 100%;
margin: 0 !important;
}
.el-button:last-child {
grid-column: span 2;
}
}
}
@media (max-width: 480px) {
.sql-dataset-container {
padding: 8px;
}
.config-aside,
.preview-main {
padding: 12px;
}
.sql-editor-container {
min-height: 150px;
max-height: 200px;
}
.form-item-card {
padding: 8px;
}
.el-input,
.el-button {
font-size: 13px !important;
}
}
</style>

<template>
云盘系统
欢迎回来
请登录您的账户
<el-form
:model="loginForm"
:rules="loginRules"
label-width="0"
ref="loginFormRef"
>
<el-form-item prop="email">
<el-input
v-model="loginForm.email"
placeholder="邮箱"
prefix-icon="el-icon-message"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="密码"
prefix-icon="el-icon-lock"
@keyup.enter.native="handleLogin"
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="loginForm.remember">记住密码</el-checkbox>
</el-form-item>
<el-form-item>
<el-button
type="primary"
class="login-btn"
@click="handleLogin"
:loading="loginLoading"
>
登录
</el-button>
</el-form-item>
</el-form>
其他登录方式
<el-button type="link" icon="el-icon-qq" class="social-btn" @click="handleQQLogin">
QQ登录
</el-button>
<el-button type="link" icon="el-icon-wechat" class="social-btn" @click="handleWechatLogin">
微信登录
</el-button>
<router-link to="/register">还没有账号? 立即注册</router-link>
忘记密码?
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
const router = useRouter();
const loginFormRef = ref();
const loginLoading = ref(false);
const loginForm = reactive({
email: '',
password: '',
remember: false
});
const loginRules = reactive({
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, message: '请输入正确的邮箱格式', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 15, message: '密码长度应为6-15位', trigger: 'blur' }
]
});
// 处理登录
const handleLogin = async () => {
(loginFormRef.value as any).validate(async valid => {
if (!valid) return;
loginLoading.value = true;
// 模拟登录请求
await new Promise(resolve => setTimeout(resolve, 1500));
// 实际项目中应调用API验证用户
ElMessage.success('登录成功,即将进入系统');
setTimeout(() => {
router.push('/dashboard');
}, 1500);
loginLoading.value = false;
});
};
// 处理第三方登录
const handleQQLogin = () => {
ElMessage.info('QQ登录功能开发中...');
};
const handleWechatLogin = () => {
ElMessage.info('微信登录功能开发中...');
};
// 处理忘记密码
const handleForgotPassword = () => {
ElMessage.info('密码找回功能开发中...');
};
// 页面加载时检查是否有记住的密码
onMounted(() => {
const savedEmail = localStorage.getItem('savedEmail');
const savedPassword = localStorage.getItem('savedPassword');
if (savedEmail && savedPassword) {
loginForm.email = savedEmail;
loginForm.password = savedPassword;
loginForm.remember = true;
}
});
</script>
<style scoped>
/* 确保根元素占满屏幕 */
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
.login-container {
min-width: 100vw;
min-height: 100vh;
background: linear-gradient(135deg, #e6f0ff, #f0f2ff);
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
padding: 20px;
}
.login-box {
width: 100%;
max-width: 400px;
background: #fff;
border-radius: 12px;
padding: 40px 30px;
box-shadow: 0 8px 24px rgba(0, 0, 255, 0.1);
text-align: center;
}
.logo {
width: 60px;
height: 60px;
background: #6b6df4;
border-radius: 50%;
color: #fff;
line-height: 60px;
font-size: 24px;
margin: 0 auto 20px;
}
h2 {
font-size: 24px;
color: #333;
margin-bottom: 8px;
}
.sub-title {
font-size: 14px;
color: #999;
margin-bottom: 30px;
}
.el-input {
--el-input-bg-color: #f8f9fe;
--el-input-border-color: transparent;
--el-input-hover-border-color: transparent;
--el-input-focus-border-color: transparent;
margin-bottom: 20px;
}
.login-btn {
width: 100%;
background: linear-gradient(90deg, #6b6df4, #8490ff);
border: none;
color: #fff;
padding: 12px 0;
border-radius: 8px;
cursor: pointer;
}
.login-btn:hover {
opacity: 0.9;
}
.social-login {
margin-top: 30px;
}
.divider {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: #eee;
}
.divider span {
color: #999;
font-size: 14px;
padding: 0 15px;
}
.social-buttons {
display: flex;
justify-content: center;
}
.social-btn {
color: #6b6df4;
font-size: 20px;
margin: 0 10px;
}
.extra-links {
display: flex;
justify-content: space-between;
margin-top: 20px;
font-size: 14px;
}
.extra-links a {
color: #6b6df4;
text-decoration: none;
}
.extra-links a:hover,
.forgot-password:hover {
text-decoration: underline;
cursor: pointer;
}
.forgot-password {
color: #6b6df4;
}
</style> 这个登录的vue组件为什么不能占满整个页面

业务画面:<template>
<CustomForm class="project-form" @submit="submitForm">
项目管理
<CustomFormItem label="項目ID:" :error="errors.projectId">
<CustomInput
v-model="ruleForm.projectId"
:disabled="pageMode === 'view' || pageMode === 'edit'"
@blur="validateField('projectId')"
:maxlength="8"
/>
</CustomFormItem>
<CustomFormItem label="項目名称:" :error="errors.projectName">
<CustomInput
v-model="ruleForm.projectName"
:disabled="pageMode === 'view'"
@blur="validateField('projectName')"
:maxlength="10"
/>
</CustomFormItem>
<CustomFormItem label="開始日:" :error="errors.startDate" class="date-item">
<CustomInputDate
v-model="ruleForm.startDate"
:disabled="pageMode === 'view'"
@blur="validateField('startDate')"
/>
</CustomFormItem>
—
<CustomFormItem label="終了日:" :error="errors.endDate" class="date-item">
<CustomInputDate
v-model="ruleForm.endDate"
:disabled="pageMode === 'view'"
@blur="validateField('endDate')"
/>
</CustomFormItem>
<CustomFormItem label="項目簡介:" :error="errors.projectDescribtion">
<CustomInputTextarea
v-model="ruleForm.projectDescribtion"
:disabled="pageMode === 'view'"
@blur="validateField('projectDescribtion')"
:maxlength="200"
:rows="3"
/>
</CustomFormItem>
<button v-if="pageMode === 'default'" type="submit">登録</button>
<button v-if="pageMode === 'edit'" type="submit">更新</button>
<button v-if="pageMode === 'edit' || pageMode === 'default'" type="button" @click="resetForm">重置</button>
</CustomForm>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import CustomForm from '@/components/zhangaipiao/customForm.vue'
import CustomFormItem from '@/components/zhangaipiao/customFormItem.vue'
import CustomInput from '@/components/zhangaipiao/inputText.vue'
import CustomInputDate from '@/components/zhangaipiao/customInputDate.vue'
import CustomInputTextarea from '@/components/zhangaipiao/customInputTextarea.vue'
import productApi from '@/api/products'
import useUserStore from '@/store/modules/user'
// 获取路由参数
const route = useRoute()
// 存储表单数据的响应式对象
const ruleForm = reactive({
projectId: '',
projectName: '',
projectDescribtion: '',
startDate: '',
endDate: '',
employeeId: '',
})
// 存储字段验证错误信息的响应式对象
const errors = reactive({
projectId: '',
projectName: '',
projectDescribtion: '',
startDate: '',
endDate: '',
})
// 页面模式
const pageMode = ref('default') // default, edit, view
// 单个字段校验函数
const validateField = (fieldName) => {
// 重置该字段的错误信息
errors[fieldName] = ''
switch (fieldName) {
case 'projectId':
if (!ruleForm.projectId) {
errors.projectId = '項目ID称は必須です'
} else if (ruleForm.projectId.length > 8) {
errors.projectId = '項目IDは8文字以内で入力してください'
}
break;
case 'projectName':
if (!ruleForm.projectName.trim()) {
errors.projectName = '項目名称は必須です'
} else if (ruleForm.projectName.trim().length > 10) {
errors.projectName = '項目名称は10文字以内で入力してください'
}
break;
case 'startDate':
if (!ruleForm.startDate) {
errors.startDate = '開始日を選択してください'
} else if (ruleForm.endDate && new Date(ruleForm.startDate) > new Date(ruleForm.endDate)) {
errors.startDate = '開始日は終了日より前でなければなりません'
}
break;
case 'endDate':
if (!ruleForm.endDate) {
errors.endDate = '終了日を選択してください'
} else if (ruleForm.startDate && new Date(ruleForm.endDate) < new Date(ruleForm.startDate)) {
errors.endDate = '終了日は開始日より後でなければなりません'
}
break;
case 'projectDescribtion':
if (!ruleForm.projectDescribtion.trim()) {
errors.projectDescribtion = '項目簡介を入力してください'
} else if (ruleForm.projectDescribtion.trim().length > 200) {
errors.projectDescribtion = '項目簡介は200文字以内で入力してください'
}
}
}
// 整体表单校验
const validateForm = () => {
let valid = true
// 校验所有字段
validateField('projectId')
validateField('projectName')
validateField('startDate')
validateField('endDate')
validateField('projectDescribtion')
// 检查是否有错误
Object.keys(errors).forEach(key => {
if (errors[key]) valid = false
})
return valid
}
// 格式化日期函数
const formatDate = (datetimeStr) => {
if (!datetimeStr) return ''
const date = new Date(datetimeStr)
return isNaN(date.getTime()) ? '' : date.toISOString().split('T')[0]
}
// 在组件挂载时加载数据
onMounted(async () => {
// const projectId = route.params.projectId || route.query.projectId
// const type = route.query.type
// if (type === 'view') {
// pageMode.value = 'view' // 查看模式:不显示按钮
// } else if (type === 'edit') {
// pageMode.value = 'edit' // 编辑模式:显示“更新”、“重置”
// } else {
// pageMode.value = 'default' // 默认模式:显示“登录”、“重置”
// }
const projectId = "123"
if ((pageMode.value === 'edit' || pageMode.value === 'view') && projectId) {
try {
const res = await productApi.getItem(projectId)
Object.assign(ruleForm, res.data) // 填充数据到 ruleForm
ruleForm.startDate = formatDate(res.data.startDate)
ruleForm.endDate = formatDate(res.data.endDate)
} catch (error) {
console.error('获取项目信息失败:', error)
alert('項目情報を取得できませんでした')
}
}
})
// 重置表单
const resetForm = () => {
Object.assign(ruleForm, {
projectId: '',
projectName: '',
projectDescribtion: '',
startDate: '',
endDate: '',
})
Object.keys(errors).forEach(key => errors[key] = '')
}
// 提交表单
const submitForm = async () => {
if (!validateForm()) return
try {
let response
// 获取用户名
const userStore = useUserStore()
const user = userStore.employeeId
ruleForm.employeeId = user
if (pageMode.value === 'edit') {
// 更新操作
response = await productApi.updateItem(ruleForm)
alert('更新成功')
} else {
// 新增操作
response = await productApi.addItem(ruleForm)
alert('登録成功')
}
console.log('API 响应:', response.data)
// 可选:提交成功后跳转页面
// router.push('/project/list')
} catch (error) {
console.error('API 请求失败:', error)
alert('提交失败,请检查网络或输入内容')
}
}
</script>
<style scoped src="@/assets/styles.css"></style>
form组件:<template>
<form class="custom-form" @submit.prevent="$emit('submit')">
<slot></slot>
</form>
</template>
<script>
export default {
emits: ['submit']
}
</script>
<style scoped src="@/assets/styles.css"></style> formItem子组件:<template>
<label v-if="label" class="form-label">{{ label }}</label>
<slot></slot>
{{ error }}
</template>
<script>
export default {
props: {
label: String,
error: String
}
}
</script>
<style scoped src="@/assets/styles.css"></style>
input组件:<template>
<input
:type="inputType"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
class="input-text"
:class="{ error: error }"
:placeholder="placeholder"
/>
{{ passwordVisible ? '隐藏' : '显示' }}
</template>
<script>
import { ref, computed } from 'vue';
export default {
props: {
modelValue: String,
type: {
type: String,
default: 'text'
},
placeholder: String,
showPassword: Boolean,
error: Boolean
},
emits: ['update:modelValue'],
setup(props) {
const passwordVisible = ref(false);
const inputType = computed(() => {
if (props.type === 'password' && passwordVisible.value) {
return 'text';
}
return props.type;
});
const togglePasswordVisibility = () => {
passwordVisible.value = !passwordVisible.value;
};
return {
passwordVisible,
inputType,
togglePasswordVisibility
};
}
}
</script>
<style scoped src="@/assets/styles.css"></style>
他们之间是怎么工作的

<template>
<el-form ref="ruleFormRef" style="margin-top: 20px;max-width: 600px" :model="ruleForm" :rules="rules"
label-width="auto" :disabled="pageMode === 'view'">
<el-form-item label="产品名称:" prop="name">
<el-input v-model="ruleForm.name" />
</el-form-item>
<el-form-item label="产品描述:" prop="description">
<el-input v-model="ruleForm.description" type="textarea"/>
</el-form-item>
<el-form-item label="产品分类:" prop="category" style="width: 50%;">
<el-select v-model="ruleForm.category" placeholder="请选择分类">
<el-option v-for="item in categorys" :label="item.name" :value="item.id" :key="item.id" />
</el-select>
</el-form-item>
<el-form-item label="产品单价:" prop="price">
<el-input v-model.number="ruleForm.price" type="number" style="width: 240px" />
</el-form-item>
<el-form-item label="产品库存:" prop="stock">
<el-input v-model.number="ruleForm.stock" type="number" style="width: 240px" />
</el-form-item>
<el-form-item label="生产厂家:" prop="manufacturer">
<el-input v-model="ruleForm.manufacturer" />
</el-form-item>
<el-form-item label="产地:" required>
<el-col :span="12">
<el-form-item prop="originProvince">
<el-select v-model="ruleForm.originProvince" placeholder="省" style="flex: 1; margin-right: 10px">
<el-option v-for="item in originProvinces" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="originCity">
<el-select v-model="ruleForm.originCity" placeholder="市" style="flex: 1 "
:disabled="!ruleForm.originProvince">
<el-option v-for="item in originCitys" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
</el-form-item>
<el-form-item label="发货地:" required>
<el-col :span="12">
<el-form-item prop="shippingProvince">
<el-select v-model="ruleForm.shippingProvince" placeholder="省" style="flex: 1; margin-right: 10px">
<el-option v-for="item in originProvinces" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="shippingCity">
<el-select v-model="ruleForm.shippingCity" placeholder="市" style="flex: 1"
:disabled="!ruleForm.shippingProvince">
<el-option v-for="item in shippingCities" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
</el-form-item>
<el-form-item label="生产日期" required>
<el-col :span="11">
<el-form-item prop="createAt">
<el-date-picker v-model="ruleForm.createAt" type="date" aria-label="Pick a date" placeholder="Pick a date"
style="width: 100%" />
</el-form-item>
</el-col>
</el-form-item>
<el-form-item>
<el-button v-if="pageMode === 'default'" style="margin-left: 18px;" type="primary" @click="submitForm(ruleFormRef, ruleForm)">
登录
</el-button>
<el-button v-if="pageMode === 'edit'" style="margin-left: 30px;" type="primary" @click="updateForm(ruleFormRef)">
更新
</el-button>
<el-button v-if="pageMode === 'edit' || pageMode === 'default'" style="margin-left: 30px;" type="primary" @click="resetForm(ruleFormRef)">
重置
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import productApi from '@/api/products'
import placeApi from '@/api/place'
import { useRouter } from 'vue-router'
import { ElLoading, ElMessage } from 'element-plus'
const pageMode = ref('') // 可选值: 'view', 'edit', 'default'
const router = useRouter();
const ruleFormRef = ref()
const categorys = ref([])// 产品分类
const originProvinces = ref([]) // 省份数据
const originCitys = ref([]) // 城市数据
const shippingCities = ref([]) // 发货地城市数据
const ruleForm = reactive({
name: '',
description: '',
category: '',
price: '',
stock: '',
manufacturer: '',
originProvince: '',
originCity: '',
shippingProvince: '',
shippingCity: '',
createAt: '',
user: 'aaa',
delivery: false,
type: [],
})
const rules = reactive({
name: [
{ required: true, message: '请输入产品名称', trigger: 'blur' },
{ max: 10, message: '长度超过10位', trigger: 'blur' },
],
description: [
{ required: true, message: '请输入产品描述', trigger: 'blur' },
{ max: 200, message: '长度超过200位', trigger: 'blur' },
],
category: [
{
required: true, message: '请选择产品分类', trigger: 'change', },
],
price: [
{
required: true,
message: '请输入产品单价',
trigger: ['blur', 'change']
},
{
validator: (rule, value, callback) => {
if (value === null || value === '') {
callback(new Error('请输入产品单价'))
} else if (value <= 0) {
callback(new Error('单价必须大于 0'))
} else if (value > 10000) {
callback(new Error('单价不能大于 10000'))
} else {
callback()
}
},
trigger: ['blur', 'change']
}
],
stock: [
{
required: true,
message: '请输入产品库存',
trigger: ['blur', 'change']
},
{
validator: (rule, value, callback) => {
if (value === null || value === '') {
callback(new Error('请输入商品单价'))
} else if (value <= 0) {
callback(new Error('单价必须大于 0'))
} else if (value > 10000) {
callback(new Error('单价不能大于 10000'))
} else {
callback()
}
},
trigger: ['blur', 'change']
}
],
manufacturer: [
{ required: true, message: '请输入生产厂家', trigger: 'blur' },
],
originProvince: {
validator: (rule, value, callback) => {
if (!value) {
callback(new Error('请选择产地省份'))
} else {
callback()
}
},
trigger: 'change'
},
originCity:{
validator: (rule, value, callback) => {
if (!value) {
callback(new Error('请选择产地城市'))
} else {
callback()
}
},
trigger: 'change'
},
shippingProvince: {
validator: (rule, value, callback) => {
if (!value) {
callback(new Error('请选择发货省份'))
} else {
callback()
}
},
trigger: 'change'
},
shippingCity:{
validator: (rule, value, callback) => {
if (!value) {
callback(new Error('请选择发货城市'))
} else {
callback()
}
},
trigger: 'change'
},
createAt: [
{
type: 'date',
required: true,
message: '请输入日期',
trigger: 'change',
},
],
})
// 重置表单操作
const resetForm = (formEl) => {
if (!formEl) return
formEl.resetFields()
}
//产品分类
function getCategory() {
productApi.getCategory().then(res => {
categorys.value = res.data;
})
}
// 获取商品详情
function getProductDetail(productId) {
productApi.getById(productId).then(res => {
const data = res.data
// 先赋值整个对象
Object.assign(ruleForm, data)
}).catch(error => {
console.error('获取商品详情失败:', error);
});
}
// 获取省份
function getProvinces() {
placeApi.getProvince().then(res => {
originProvinces.value = res.data
}).catch(error => {
console.error('获取省份失败:', error)
})
}
// 监听产地省份变化,加载城市
watch(
() => ruleForm.originProvince,
(newVal) => {
if (newVal) {
placeApi.getCity(newVal).then(res => {
originCitys.value = res.data
}).catch(() => {
originCitys.value = []
})
} else {
// 如果省份为空,则清空城市列表和当前选中的城市字段
originCitys.value = []
ruleForm.originCity = ''
}
}
)
// 监听发货地省份变化,加载城市
watch(
() => ruleForm.shippingProvince,
(newVal) => {
if (newVal) {
placeApi.getCity(newVal).then(res => {
shippingCities.value = res.data
}).catch(() => {
shippingCities.value = []
})
} else {
// 如果省份为空,则清空城市列表和当前选中的城市字段
shippingCities.value = []
ruleForm.shippingCity = ''
}
}
)
// 更新操作
const updateForm = async (formEl) => {
if (!formEl) return
const loading = ElLoading.service({ fullscreen: true })
await formEl.validate((valid, fields) => {
if (valid) {
console.log('开始提交...')
// 调用 API 更新商品信息
productApi.update(ruleForm).then(res => {
ElMessage.success('更新成功')
router.push('/home/test1') // 可选跳转回列表页
}).catch(error => {
console.error('更新失败:', error)
ElMessage.error('更新失败,请重试')
}).finally(() => {
loading.close()
})
} else {
console.log('表单验证失败:', fields)
ElMessage.error('请检查输入内容')
loading.close()
return false
}
})
}
// 登录操作
const submitForm = async (formEl, ruleForm) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
console.log('开始提交商品...')
const loading = ElLoading.service({ fullscreen: true })
productApi.addProduct(ruleForm).then(res => {
ElMessage.success('登录成功')
router.push('/home/test1')
}).catch(error => {
console.error('登录失败:', error)
ElMessage.error('登录失败,请重试')
}).finally(() => {
loading.close()
})
} else {
console.log('表单验证失败:', fields)
ElMessage.error('请检查输入内容')
loading.close()
return false
}
})
}
onMounted(() => {
getCategory()
getProvinces()
// 获取当前路由信息
const route = router.currentRoute.value
// 判断页面是查看、编辑还是新增模式
const type = route.query.type
const productId = route.params.id || route.query.id
if (type === 'view') {
pageMode.value = 'view' // 查看模式:不显示按钮
} else if (type === 'edit') {
pageMode.value = 'edit' // 编辑模式:显示“更新”、“重置”
} else {
pageMode.value = 'default' // 默认模式:显示“登录”、“重置”
}
// 如果存在商品 ID,则调用接口获取商品详情并填充表单
if (productId) {
getProductDetail(productId)
}
})
</script>
<style>
.location-selectors {
display: flex;
justify-content: space-between;
width: 100%;
}
</style> 我想将form表单拆分成五层自定义组件结构

<template>
<el-form
:model="queryParams"
ref="queryForm"
:inline="true"
v-show="showSearch"
label-width="68px"
>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['portal:otdhtml:add']"
>新增</el-button
>
</el-col>
</el-row>
<el-table
v-loading="loading"
:data="otdhtmlList"
@selection-change="handleSelectionChange"
>
<el-table-column label="标题" align="center" prop="titlt">
<template slot-scope="scope">
{{ scope.row.title }}
</template>
</el-table-column>
<el-table-column label="期号" align="center" prop="expressNo">
<template slot-scope="scope">
{{ scope.row.expressNo }}
</template>
</el-table-column>
<el-table-column label="上线状态" align="center" prop="flag">
<template slot-scope="scope">
<el-switch
v-hasPermi="['portal:banner:edit']"
v-model="scope.row.flag"
active-value="1"
inactive-value="0"
@change="handleFlagChange(scope.row)"
></el-switch>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
class-name="small-padding fixed-width"
>
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handlePreview(scope.row)"
v-hasPermi="['portal:otdhtml:edit']"
>预览</el-button
>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['portal:otdhtml:edit']"
>修改</el-button
>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['portal:otdhtml:remove']"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</template>
<script>
import {
listOtdhtml,
getOtdhtml,
delOtdhtml,
addOtdhtml,
updateOtdhtml,
exportOtdhtml,
} from "@/api/portal/otdhtml";
import Editor from "@/components/Editor";
export default {
name: "Otdhtml",
components: { Editor },
data() {
return {
title: "",
expressNo: "",
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// OTDhtml表格数据
otdhtmlList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
content: null,
templateJson: null,
flag: null,
title: null,
expressNo: null,
},
//状态选择
// flagOptions: [
// { value: "0", label: "未发布" },
// { value: "1", label: "已上线" },
// { value: "2", label: "以往发布" },
// { value: "3", label: "审核未通过" },
// ],
// 表单参数
form: {},
// 表单校验
rules: {},
};
},
created() {
this.getList();
},
methods: {
/** 查询OTDhtml列表 */
getList() {
this.loading = true;
listOtdhtml(this.queryParams).then((response) => {
console.log("response.rows", response.rows);
this.otdhtmlList = response.rows;
for (let i = 0; i < this.otdhtmlList.length; i++) {
var rowData = response.rows[i];
this.templateJson = JSON.parse(rowData.templateJson);
rowData.title = this.templateJson.formData.title;
rowData.expressNo = this.templateJson.formData.expressNo;
}
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
//预览跳转
handlePreview(row) {
const id = row.id;
let host = window.location.host;
if (host == "10.58.136.26") {
//uat跳转地址
window.open("http://10.58.136.26/otd/express?id=" + id);
} else {
//生产跳转地址
window.open("https://55555.bba/otd/express?id=" + id);
}
},
// 表单重置
reset() {
this.form = {
id: null,
content: null,
templateJson: null,
createTime: null,
flag: null,
remark: null,
createBy: null,
updateBy: null,
};
this.resetForm("form");
},
//、
handleFlagChange(row) {
let text = row.flag === "1" ? "启用" : "停用";
this.$confirm("确认要" + text + "吗?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(function () {
return updateOtdhtml(row);
})
.then(() => {
this.msgSuccess(text + "成功");
console.log("row", row);
this.getList();
})
.catch(function () {
row.flag = row.flag === "0" ? "1" : "0";
});
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.queryParams.flag = "";
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map((item) => item.id);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 新增按钮操作 */
handleAdd() {
this.$router.push({
path: "/otdPreview/saveEmail",
});
},
/** 修改按钮操作 */
handleUpdate(row) {
if (row.flag == 1) {
this.msgError("不可修改已上线的资讯");
return;
}
const id = row.id;
this.$router.push({
name: "editEmail",
params: { id: row.id },
});
// this.reset();
// const id = row.id || this.ids;
// getOtdhtml(id).then((response) => {
// this.form = response.data;
// this.open = true;
// this.title = "修改OTDhtml";
// });
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate((valid) => {
if (valid) {
if (this.form.id != null) {
updateOtdhtml(this.form).then((response) => {
this.msgSuccess("修改成功");
this.open = false;
this.getList();
});
} else {
addOtdhtml(this.form).then((response) => {
this.msgSuccess("新增成功");
this.open = false;
this.getList();
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
if (row.flag == 1) {
this.msgError("不可删除已上线的资讯");
return;
}
const ids = row.id || this.ids;
this.$confirm("是否确认删除?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(function () {
return delOtdhtml(ids);
})
.then(() => {
this.getList();
this.msgSuccess("删除成功");
});
},
formmat(flag) {
if (flag == 0) {
return "未发布";
} else if (flag == 1) {
return "已上线";
} else if (flag == 2) {
return "以往发布";
} else {
return "审核未通过";
}
},
// /** 导出按钮操作 */
// handleExport() {
// const queryParams = this.queryParams;
// this.$confirm("是否确认导出所有OTDhtml数据项?", "警告", {
// confirmButtonText: "确定",
// cancelButtonText: "取消",
// type: "warning",
// })
// .then(function () {
// return exportOtdhtml(queryParams);
// })
// .then((response) => {
// this.download(response.msg);
// });
// },
},
};
</script>
<style lang="scss" scope>
.content2 {
// 3行文本超长换行
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
line-clamp: 1;
box-orient: vertical;
cursor: pointer;
}
</style>
换成vue3

<template>
<el-row :gutter="20">
<el-col :span="24">
<el-form :inline="true" label-width="100px" @submit.prevent="getList">
<el-form-item label="名称">
<el-input v-model="queryParams.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="责任人">
<el-input v-model="queryParams.respPerson" placeholder="请输入责任人" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-col>
<el-col :span="24">
<el-button
type="primary"
@click="toggleGantt"
style="margin-bottom: 15px;"
>
{{ showGantt ? '收起甘特图' : '展开甘特图' }}
</el-button>
</el-col>
<el-col :span="showGantt ? 12 : 24">
<el-table
ref="table"
:data="listData"
row-key="uid"
border
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@row-click="rowClick"
@expand-change="handleExpandChange"
>
<el-table-column prop="code" label="编号" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="respPerson" label="责任人" />
<el-table-column prop="schedule" label="完成百分比" />
<el-table-column prop="planStartDate" label="计划开始日期" />
<el-table-column prop="planEndDate" label="计划结束日期" />
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" icon="el-icon-view" @click="handleUpdate(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
layout="prev, pager, next"
:total="total"
:page-size="queryParams.pageSize"
@current-change="handleCurrentChange"
/>
</el-col>
<el-col :span="12" v-if="showGantt">
</el-col>
</el-row>
<el-dialog :title="title" :visible.sync="open" width="850px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px" :disabled="disable">
<el-row>
<el-col :span="12">
<el-form-item label="编号" prop="code">
<el-input v-model="form.code" placeholder="请输入编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="备注" prop="remarks">
<el-input v-model="form.remarks" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
<el-button @click="cancel">取 消</el-button>
</el-form>
</el-dialog>
</template>
<script>
import gantt from 'dhtmlx-gantt';
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
import { listPlan, getPlan } from '@/api/dw/plan/planview';
export default {
name: 'Planview',
data() {
return {
listData: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
name: null,
respPerson: null
},
open: false,
title: '',
form: {},
rules: {
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
schedule: [
{ required: true, message: '完成百分比不能为空', trigger: 'blur' },
{ type: 'number', message: '输入内容不是有效的数字', trigger: 'blur' }
]
},
disable: true,
showGantt: true, // 控制甘特图显示
flatData: [], // 扁平化数据
baseDate: new Date('2023-01-01'), // 基准日期
maxDuration: 365, // 最大工期(天)
maxOffset: 365 // 最大偏移天数
};
},
mounted() {
this.getList();
this.initGantt();
},
methods: {
async getList() {
const res = await listPlan(this.queryParams);
this.listData = this.handleTree(res.data, 'uid', 'parentUid');
this.total = res.total;
this.flatData = this.flattenTree(this.listData);
this.getMaxDuration();
this.$nextTick(() => {
const tasks = this.ganttData(this.flatData);
this.updateGantt(tasks);
});
},
// 获取最大工期
getMaxDuration() {
const durations = this.flatData.map(item => item.planDuration || 0);
this.maxDuration = Math.max(...durations, 1);
},
// 计算甘特图宽度
calculateGanttWidth(row) {
const duration = row.planDuration || 0;
const width = (duration / this.maxDuration) * 100;
return ${Math.max(5, width)}%;
},
// 计算甘特图偏移
calculateGanttOffset(row) {
if (!row.planStartDate) return '0%';
const startDate = new Date(row.planStartDate);
const daysOffset = Math.floor((startDate - this.baseDate) / (1000 * 60 * 60 * 24));
return ${(daysOffset / this.maxOffset) * 100}%;
},
// 初始化甘特图
initGantt() {
if (!this.$refs.ganttContainer) return;
gantt.config.date_format = '%Y-%m-%d';
gantt.config.columns = [
{ name: 'text', label: '任务名称', tree: true, width: '*' },
{ name: 'start_date', label: '开始时间', align: 'center' },
{ name: 'duration', label: '工期(天)', align: 'center' }
];
gantt.templates.task_text = (start, end, task) => task.text;
gantt.init(this.$refs.ganttContainer);
gantt.parse({ data: [], links: [] });
// 确保事件监听器只绑定一次
if (!this.ganttEventInitialized) {
gantt.attachEvent('onTaskSelected', id => {
const row = this.flatData.find(item => item.uid === id);
if (row) {
this.$refs.table.setCurrentRow(row);
}
});
this.ganttEventInitialized = true;
}
},
// 更新甘特图
updateGantt(tasks) {
gantt.clearAll();
gantt.parse({ data: tasks, links: [] });
},
// 树形结构转扁平结构
flattenTree(data) {
const result = [];
const stack = [...data];
while (stack.length) {
const node = stack.pop();
result.push(node);
if (node.children) {
stack.push(...node.children);
}
}
return result;
},
// 转换为甘特图数据
ganttData(data) {
return data
.filter(item => item.uid && item.planStartDate)
.map(item => ({
id: item.uid,
text: item.name,
start_date: item.planStartDate,
duration: item.planDuration || 0,
progress: (item.schedule || 0) / 100,
parent: item.parentUid || 0
}));
},
// 处理树形结构
handleTree(data, idKey = 'id', parentKey = 'parentId') {
const map = {};
const tree = [];
data.forEach(item => (map[item[idKey]] = item));
data.forEach(item => {
const parent = map[item[parentKey]];
if (parent) {
(parent.children || (parent.children = [])).push(item);
} else {
tree.push(item);
}
});
return tree;
},
// 行点击事件
rowClick(row) {
const taskId = row.uid;
this.$nextTick(() => {
if (gantt.$initialized) {
gantt.showTask(taskId);
gantt.selectTask(taskId);
// 强制重绘确保高亮生效
gantt.render();
}
});
},
// 树展开/折叠更新甘特图
handleExpandChange(row, expanded) {
if (expanded) {
const allChildren = this.getAllChildren(row);
const tasks = this.ganttData(allChildren);
this.$nextTick(() => {
this.updateGantt(tasks);
if (gantt.$initialized && tasks.length > 0) {
// 获取展开节点的最早和最晚日期
const dates = tasks
.filter(t => t.start_date)
.map(t => new Date(t.start_date));
if (dates.length > 0) {
const minDate = new Date(Math.min(...dates.map(d => d.getTime())));
const maxDate = new Date(Math.max(...dates.map(d => {
const endDate = new Date(t.start_date);
endDate.setDate(endDate.getDate() + (t.duration || 0));
return endDate.getTime();
})));
// 设置视图时间范围
gantt.setWorkTime({
start_date: minDate,
end_date: maxDate
});
// 调整视图缩放级别
gantt.config.scale_unit = 'day';
gantt.config.step = 1;
gantt.config.scale_height = 28;
// 重新渲染并定位第一个任务
gantt.render();
gantt.showTask(tasks[0].id);
gantt.selectTask(tasks[0].id);
}
}
});
} else {
const topLevelTasks = this.listData.map(item => ({
id: item.uid,
text: item.name,
start_date: item.planStartDate,
duration: item.planDuration || 0,
progress: (item.schedule || 0) / 100,
parent: item.parentUid || 0
}));
this.$nextTick(() => {
this.updateGantt(topLevelTasks);
if (gantt.$initialized) {
// 恢复默认时间范围
gantt.setWorkTime({
start_date: new Date('2023-01-01'),
end_date: new Date('2023-12-31')
});
gantt.config.scale_unit = 'month';
gantt.config.step = 1;
gantt.config.scale_height = 28;
gantt.render();
}
});
}
},
// 递归获取所有子节点
getAllChildren(node) {
let children = [node];
if (node.children && node.children.length > 0) {
node.children.forEach(child => {
children = children.concat(this.getAllChildren(child));
});
}
return children;
},
// 切换甘特图
toggleGantt() {
this.showGantt = !this.showGantt;
if (this.showGantt) {
this.$nextTick(() => {
const tasks = this.ganttData(this.flatData);
this.updateGantt(tasks);
});
}
},
// 获取数据
async handleUpdate(row) {
const res = await getPlan(row.uid);
this.form = res.data;
this.open = true;
this.title = '查看治理计划';
},
// 取消按钮
cancel() {
this.open = false;
},
// 重置查询
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
name: null,
respPerson: null
};
this.getList();
},
// 分页切换
handleCurrentChange(page) {
this.queryParams.pageNum = page;
this.getList();
}
}
};
</script>
<style scoped>
.page-container {
padding: 20px;
}
.table-container {
background-color: #fff;
padding: 10px;
border-radius: 4px;
}
.gantt-container {
background-color: #f9f9f9;
border: 1px solid #ebeef5;
padding: 10px;
border-radius: 4px;
}
.dialog-footer {
text-align: right;
}
.search-wrapper {
margin-bottom: 20px;
background-color: #fff;
padding: 10px;
border-radius: 4px;
}
.toggle-button {
margin-bottom: 15px;
}
.gantt-bar-container {
position: relative;
height: 30px;
background-color: #f5f7fa;
border-radius: 4px;
overflow: hidden;
margin: 5px 0;
}
.gantt-bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: #409EFF;
color: white;
text-align: center;
font-size: 12px;
line-height: 30px;
}
</style>
列表的展开和收缩,甘特图没有同步,请重新优化

<template>
<el-card class="login-card">
系统登录
<el-form :model="loginForm" :rules="loginRules" ref="formRef" @submit.prevent="handleLogin">
<el-form-item prop="username">
<el-input v-model="loginForm.username" placeholder="请输入用户名" prefix-icon="User" />
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
prefix-icon="Lock"
show-password
/>
</el-form-item>
<el-button type="primary" class="login-btn" native-type="submit" :loading="loading">
登 录
</el-button>
</el-form>
</el-card>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { loginApi } from '@/api/login' // 根据你的实际路径调整
const router = useRouter()
const formRef = ref<FormInstance>()
const loading = ref(false)
// 登录表单数据
const loginForm = reactive({
username: '',
password: '',
})
// 表单验证规则
const loginRules = reactive<FormRules>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
],
})
// 处理登录提交
const handleLogin = async () => {
// 表单验证
const valid = await formRef.value?.validate()
if (!valid) return
loading.value = true
try {
// 调用登录API
const response = await loginApi(loginForm)
// ✅ 正确方式:从 response.data 中获取字段
const { code, data, msg } = response.data
// 登录成功处理
ElMessage.success(msg || '登录成功')
// 存储token到localStorage
localStorage.setItem('token', data.token)
// 跳转到首页
router.push('/index')
// 其他错误处理
ElMessage.error(msg || '登录失败,请重试')
} catch (error: any) {
// 401错误处理
ElMessage.error('用户名或密码错误')
ElMessage.error('登录请求失败:' + (error.message || '未知错误'))
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.login-card {
width: 400px;
padding: 30px;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
}
.login-title {
text-align: center;
margin-bottom: 30px;
color: #409eff;
}
.login-btn {
width: 100%;
margin-top: 10px;
height: 45px;
font-size: 16px;
}
</style>
import { createRouter, createWebHistory } from 'vue-router'
/* 配置路由的第一种方式:导入对应的组件 */
/* import MainLayout from '@/views/layout/MainLayout.vue' */
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: '',
component: () => import('@/views/layout/MainLayout.vue'),
/* 重定向 */
redirect: '/index',
children: [
//path:代表路径 name:代表访问的时候访问的名字 component:就是访问的组件
{ path: 'clazz', component: () => import('@/views/clazz/ClazzIndex.vue') },
{ path: 'dept', component: () => import('@/views/dept/DepIndex.vue') },
{ path: 'emp', component: () => import('@/views/emp/EmpIndex.vue') },
{ path: 'stu', component: () => import('@/views/stu/StuIndex.vue') },
{ path: 'index', component: () => import('@/views/index/HomeIndex.vue') },
{ path: 'log', component: () => import('@/views/log/LogIndex.vue') },
{ path: 'report/emp', component: () => import('@/views/report/emp/EmpIndex.vue') },
{ path: 'report/stu', component: () => import('@/views/report/stu/StuIndex.vue') },
],
},
{ path: '/login', component: () => import('@/views/login/LoginIndex.vue') },
],
})
export default router
登录成功后没有跳转到/index页面检查下什么问题