Browse Source

上传图片

master
zhanglei 4 hours ago
parent
commit
f7329e391e
  1. 113
      api_generator_vue.py
  2. 109
      templates/vue/index.vue.j2
  3. 10
      vue-vben-admin/apps/web-antd/src/api/act_log.ts
  4. 27
      vue-vben-admin/apps/web-antd/src/api/device.ts
  5. 18
      vue-vben-admin/apps/web-antd/src/api/fun.ts
  6. 7
      vue-vben-admin/apps/web-antd/src/api/user.ts
  7. 4
      vue-vben-admin/apps/web-antd/src/router/routes/modules/fun.ts
  8. 10
      vue-vben-admin/apps/web-antd/src/views/device/data.ts
  9. 10
      vue-vben-admin/apps/web-antd/src/views/device/form.ts
  10. 634
      vue-vben-admin/apps/web-antd/src/views/device/index.vue
  11. 178
      vue-vben-admin/apps/web-antd/src/views/enums/data.ts
  12. 6
      vue-vben-admin/apps/web-antd/src/views/fun/data.ts
  13. 6
      vue-vben-admin/apps/web-antd/src/views/fun/form.ts
  14. 514
      vue-vben-admin/apps/web-antd/src/views/fun/index.vue
  15. 53
      vue-vben-admin/apps/web-antd/src/views/user/index.vue

113
api_generator_vue.py

@ -0,0 +1,113 @@
import os
from jinja2 import Environment, FileSystemLoader
from db import get_table, get_columns
from utils import *
import yaml
env = Environment(loader=FileSystemLoader("templates/vue"))
def render(template, out, ctx):
tpl = env.get_template(template)
content = tpl.render(**ctx)
os.makedirs(os.path.dirname(out), exist_ok=True)
with open(out, "w", encoding="utf-8") as f:
f.write(content)
def build_fields(table):
cols = get_columns(table)
fields = []
for c in cols:
field = {}
field["name"] = to_camel(c["column_name"])
field["comment"] = c["column_comment"]
field["type"] = c["data_type"]
# ⭐ 在这里调用组件解析
field["component"] = parse_component(c)
fields.append(field)
return fields
def generate(table):
with open("./config.yml", "r", encoding="utf-8") as f:
cfg = yaml.safe_load(f)
cfg = resolve_config(cfg)
API_DIR = cfg["frontend"]["root"]+"/"+cfg["frontend"]["api"]
VIEW_DIR = cfg["frontend"]["root"]+"/"+cfg["frontend"]["views"]
ROUTER_DIR = cfg["frontend"]["root"]+"/"+cfg["frontend"]["router"]
MOCK_DIR = cfg["frontend"]["root"]+"/"+cfg["frontend"]["mock"]
EDIT_FIELDS = cfg["frontend"]["editFields"]
table_info = get_table(table)
entity = table.replace("health_","")
ctx = {
"table":table,
"table_comment":table_info["table_comment"],
"old_table": to_kebab(table),
"entity":entity,
"editFields": to_class_join(EDIT_FIELDS),
"fields":build_fields(table)
}
render(
"api.ts.j2",
f"{API_DIR}/{entity}.ts",
ctx
)
render(
"index.vue.j2",
f"{VIEW_DIR}/{entity}/index.vue",
ctx
)
render(
"data.ts.j2",
f"{VIEW_DIR}/{entity}/data.ts",
ctx
)
render(
"form.ts.j2",
f"{VIEW_DIR}/{entity}/form.ts",
ctx
)
render(
"router.ts.j2",
f"{ROUTER_DIR}/{entity}.ts",
ctx
)
render(
"mock.ts.j2",
f"{MOCK_DIR}/{entity}.ts",
ctx
)
# ... existing code ...
if __name__ == "__main__":
import sys
# 从命令行参数获取表名
if len(sys.argv) > 1:
table_name = sys.argv[1]
print(f"=== 生成前端代码1:{table_name} ===")
generate(table_name)
else:
# 如果没有提供参数,提示用户输入
table_name = input("请输入表名 (例如 health_user): ").strip()
if table_name:
print(f"=== 生成前端代码2:{table_name} ===")
generate(table_name)
else:
print("❌ 未输入表名,退出程序")

109
templates/vue/index.vue.j2

@ -22,6 +22,8 @@ const uploadVisible = ref(false);
const uploadFieldName = ref('');
const uploadImageUrl = ref('');
const uploadedUrl = ref(''); // 上传成功后返回的
const formUploadUrls = ref<Record<string, string>>({}); // 存储新增弹窗中各字段的上传 URL
// ========== 动态生成的查询表单 Schema ==========
const querySchema = ref([]);
// ========== 枚举数据配置 ==========
@ -144,7 +146,7 @@ const gridOptions = {
width: 200,
align: 'center',
slots: {
default: ({ row }: any) => {
default: ({row}: any) => {
const imgUrl = row[col.field];
// 如果没有图片,显示提示文字
@ -345,20 +347,30 @@ const [Modal, modalApi] = useVbenModal({
const values = await formApi.validateAndSubmitForm();
if (!values) return;
// 合并上传的 URL 到提交数据中(只在新增模式下)
const submitValues = !isEdit.value
? {...values, ...formUploadUrls.value}
: values;
// 处理日期格式,将 Day.js 对象转换为字符串
const submitValues = {
...values,
userBirthday: values.userBirthday ? dayjs(values.userBirthday).format('YYYY-MM-DD') : null,
const finalSubmitValues = {
...submitValues,
userBirthday: submitValues.userBirthday ? dayjs(submitValues.userBirthday).format('YYYY-MM-DD') : null,
};
if (isEdit.value && currentRow.value?.id) {
await {{entity}}Api.save({...submitValues, id: currentRow.value.id});
await {{entity}}Api.save({...finalSubmitValues, id: currentRow.value.id});
} else {
await {{entity}}Api.add(submitValues);
await {{entity}}Api.add(finalSubmitValues);
}
modalApi.close();
gridApi.reload();
// 重置上传 URL 记录
if (!isEdit.value) {
formUploadUrls.value = {};
}
} catch (error) {
console.error('保存失败:', error);
}
@ -366,6 +378,7 @@ const [Modal, modalApi] = useVbenModal({
onOpenChange(isOpen: boolean) {
if (!isOpen && !isEdit.value) {
formApi.resetForm();
formUploadUrls.value = {};
}
},
})
@ -448,7 +461,7 @@ const [UploadModal, uploadModalApi] = useVbenModal({
})
// ========== 动态计算编辑表单 Schema ==========
// 新增时:过滤掉 id 字段
// 新增时:过滤掉 id 字段,并为图片字段添加上传按钮
// 编辑时:包含 id 字段
const getEditFormSchema = () => {
return formSchema.filter(item => {
@ -461,9 +474,64 @@ const getEditFormSchema = () => {
return true;
}
return false;
}).map(item => {
// 只在新增模式下,且是图片字段,添加上传组件
if (!isEdit.value && isImageField(item.fieldName)) {
// 优先使用 formUploadUrls 中存储的 URL,其次使用表单值
const currentImageUrl = formUploadUrls.value[item.fieldName] || '';
return {
...item,
component: 'Upload',
componentProps: {
class: 'w-full',
accept: 'image/*',
maxCount: 1,
listType: 'picture-card',
fileList: currentImageUrl && typeof currentImageUrl === 'string' ? [{
uid: '-1',
name: 'image.png',
status: 'done',
url: currentImageUrl,
}] : (Array.isArray(currentImageUrl) ? currentImageUrl : []),
beforeUpload: async (file: any) => {
console.log('📦 新增表单内上传文件:', file);
try {
const formData = new FormData();
formData.append('file', file);
// 调用上传 API
const res = await {{entity}}Api.upload(formData);
const resultUrl = res.result || res.message || res.url || res;
console.log(item.fieldName + ' 新增表单内上传成功,URL:', resultUrl);
message.success('上传成功!');
// 保存上传返回的 URL 到专用存储
formUploadUrls.value[item.fieldName] = resultUrl;
// 上传成功后,重新生成 schema 以更新 fileList 显示
setTimeout(() => {
formApi.setState({
schema: getEditFormSchema(),
});
}, 0);
// Deleted:formApi.resetForm();
// 返回 false 阻止默认上传行为
return false;
} catch (error: any) {
console.error('❌ 新增表单内上传失败:', error);
message.error(error?.message || '上传失败');
return false;
}
},
},
};
}
return item;
});
};
// ========== 表单配置 ==========
const [Form, formApi] = useVbenForm({
schema: editFormSchema,
@ -497,7 +565,7 @@ function openUploadDialog(fieldName: string, currentUrl: string) {
// ========== 处理图片上传 - 使用 custom-request ==========
async function handleCustomRequest(options: any) {
const { file, onSuccess, onError } = options;
const {file, onSuccess, onError} = options;
try {
console.log('📦 开始上传文件:', file);
@ -531,7 +599,7 @@ async function handleCustomRequest(options: any) {
}];
// 通知组件上传成功
onSuccess({ url: resultUrl });
onSuccess({url: resultUrl});
} catch (error: any) {
console.error('❌ 上传失败:', error);
@ -542,7 +610,7 @@ async function handleCustomRequest(options: any) {
// ========== 上传成功或失败后的处理
function handleChange(info: any) {
const { status } = info.file;
const {status} = info.file;
console.log('📋 文件状态变化:', status, info.file);
if (status === 'done') {
@ -558,6 +626,8 @@ function handleChange(info: any) {
function handleAdd() {
isEdit.value = false;
modalTitle.value = '功能配置表新增';
// 重置上传 URL 记录
formUploadUrls.value = {};
// 更新表单 schema (不包含 id)
formApi.setState({
schema: getEditFormSchema(),
@ -666,11 +736,14 @@ function handleReset() {
:file-list="uploadFileList"
:custom-request="handleCustomRequest"
:show-upload-list="true"
accept="image/*" style="width: 100%;"
accept="image/*" style="width: 100%;"
>
<div style="width: 100%; height: 150px; border: 2px dashed #d9d9d9; border-radius: 8px; background: #fafafa; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer;">
<div
style="width: 100%; height: 150px; border: 2px dashed #d9d9d9; border-radius: 8px; background: #fafafa; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer;">
<!-- 使用 SVG 图标代替 -->
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom: 8px;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#1890ff"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="margin-bottom: 8px;">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
@ -685,17 +758,19 @@ function handleReset() {
<p style="color: #67c23a; font-size: 14px; margin-bottom: 8px;">✅ 上传成功!</p>
<p style="color: #999; font-size: 12px; margin-bottom: 8px;">新图片预览:</p>
<Image
:src="uploadedUrl" style="max-width: 200px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"
:src="uploadedUrl"
style="max-width: 200px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"
:preview="true"
/>
<p style="color: #999; font-size: 12px; margin-top: 8px; word-break: break-all;">{{ uploadedUrl }}</p>
<p style="color: #999; font-size: 12px; margin-top: 8px; word-break: break-all;"></p>
</div>
<!-- 展示原图(如果有) -->
<div v-if="uploadImageUrl && !uploadedUrl" style="margin-top: 16px; text-align: center;">
<p style="color: #999; font-size: 12px; margin-bottom: 8px;">当前图片:</p>
<Image
:src="uploadImageUrl.startsWith('http') || uploadImageUrl.startsWith('/') ? uploadImageUrl : `${API_BASE_URL}${uploadImageUrl}`" style="max-width: 200px; border-radius: 4px;"
:src="uploadImageUrl.startsWith('http') || uploadImageUrl.startsWith('/') ? uploadImageUrl : `${API_BASE_URL}${uploadImageUrl}`"
style="max-width: 200px; border-radius: 4px;"
:preview="true"
/>
</div>

10
vue-vben-admin/apps/web-antd/src/api/act_log.ts

@ -5,6 +5,7 @@
import { requestClient } from '#/api/request';
import { useAppConfig } from '@vben/hooks';
import { useAccessStore } from '@vben/stores';
export namespace act_logApi {
@ -16,7 +17,7 @@ export namespace act_logApi {
*/
export function page(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-act-log/page', params,
{ headers: {'Content-Type': 'application/json', Token: '917e9898-8a0a-4079-a16a-e456457e070c', version: '1.0.1', familyId: 0}});
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
@ -31,7 +32,7 @@ export namespace act_logApi {
*/
export function add(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-act-log/add', data,
{ headers: {'Content-Type': 'application/json', Token: '917e9898-8a0a-4079-a16a-e456457e070c', version: '1.0.1', familyId: 0}});
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
@ -39,7 +40,7 @@ export namespace act_logApi {
*/
export function save(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-act-log/modify', data,
{ headers: {'Content-Type': 'application/json', Token: '917e9898-8a0a-4079-a16a-e456457e070c', version: '1.0.1', familyId: 0}});
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
@ -53,7 +54,8 @@ export namespace act_logApi {
*
*/
export function enumList(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-enums/optionList', params,{ headers: {'Content-Type': 'application/json', Token: '917e9898-8a0a-4079-a16a-e456457e070c', version: '1.0.1'}});
return requestClient.post(applicationConfig.javaURL+'/health-enums/optionList', params,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
}

27
vue-vben-admin/apps/web-antd/src/api/device.ts

@ -5,6 +5,7 @@
import { requestClient } from '#/api/request';
import { useAppConfig } from '@vben/hooks';
import { useAccessStore } from '@vben/stores';
export namespace deviceApi {
@ -16,7 +17,7 @@ export namespace deviceApi {
*/
export function page(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-device/page', params,
{ headers: {'Content-Type': 'application/json', Token: 'ded93460-0cf5-45db-81ae-7608dbd3f51e', version: '1.0.1', familyId: 0}});
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
@ -27,10 +28,19 @@ export namespace deviceApi {
}
/**
* /
*
*/
export function add(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-device/add', data,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function save(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-device', data);
return requestClient.post(applicationConfig.javaURL+'/health-device/modify', data,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
@ -44,7 +54,16 @@ export namespace deviceApi {
*
*/
export function enumList(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-enums/optionList', params,{ headers: {'Content-Type': 'application/json', Token: 'ded93460-0cf5-45db-81ae-7608dbd3f51e', version: '1.0.1'}});
return requestClient.post(applicationConfig.javaURL+'/health-enums/optionList', params,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function upload(params: any) {
return requestClient.post(applicationConfig.javaURL+'/file/up', params,
{ headers: {'Content-Type': 'multipart/form-data', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
}

18
vue-vben-admin/apps/web-antd/src/api/fun.ts

@ -5,6 +5,7 @@
import { requestClient } from '#/api/request';
import { useAppConfig } from '@vben/hooks';
import { useAccessStore } from '@vben/stores';
export namespace funApi {
@ -16,7 +17,7 @@ export namespace funApi {
*/
export function page(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-fun/page', params,
{ headers: {'Content-Type': 'application/json', Token: 'ded93460-0cf5-45db-81ae-7608dbd3f51e', version: '1.0.1', familyId: 0}});
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
@ -31,7 +32,7 @@ export namespace funApi {
*/
export function add(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-fun/add', data,
{ headers: {'Content-Type': 'application/json', Token: 'ded93460-0cf5-45db-81ae-7608dbd3f51e', version: '1.0.1', familyId: 0}});
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
@ -39,7 +40,7 @@ export namespace funApi {
*/
export function save(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-fun/modify', data,
{ headers: {'Content-Type': 'application/json', Token: 'ded93460-0cf5-45db-81ae-7608dbd3f51e', version: '1.0.1', familyId: 0}});
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
@ -53,7 +54,16 @@ export namespace funApi {
*
*/
export function enumList(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-enums/optionList', params,{ headers: {'Content-Type': 'application/json', Token: 'ded93460-0cf5-45db-81ae-7608dbd3f51e', version: '1.0.1'}});
return requestClient.post(applicationConfig.javaURL+'/health-enums/optionList', params,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function upload(params: any) {
return requestClient.post(applicationConfig.javaURL+'/file/up', params,
{ headers: {'Content-Type': 'multipart/form-data', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
}

7
vue-vben-admin/apps/web-antd/src/api/user.ts

@ -32,7 +32,7 @@ export namespace userApi {
*/
export function add(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-user/add', data,
{ headers: {'Content-Type': 'application/json', Token: 'ded93460-0cf5-45db-81ae-7608dbd3f51e', version: '1.0.1'}});
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
@ -40,7 +40,7 @@ export namespace userApi {
*/
export function save(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-user/modify', data,
{ headers: {'Content-Type': 'application/json', Token: 'ded93460-0cf5-45db-81ae-7608dbd3f51e', version: '1.0.1'}});
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
@ -54,7 +54,8 @@ export namespace userApi {
*
*/
export function enumList(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-enums/optionList', params,{ headers: {'Content-Type': 'application/json', Token: 'ded93460-0cf5-45db-81ae-7608dbd3f51e', version: '1.0.1'}});
return requestClient.post(applicationConfig.javaURL+'/health-enums/optionList', params,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
}

4
vue-vben-admin/apps/web-antd/src/router/routes/modules/fun.ts

@ -8,7 +8,7 @@ const routes: RouteRecordRaw[] = [
{
path: '/fun',
name: '功能配置模块',
name: '功能配置模块',
meta: {
icon: 'ic:baseline-view-in-ar',
order: 1000,
@ -18,7 +18,7 @@ const routes: RouteRecordRaw[] = [
children: [
{
meta: {
title: "功能配置列表",
title: "功能配置列表",
},
name: 'funList',
path: '/fun',

10
vue-vben-admin/apps/web-antd/src/views/device/data.ts

@ -53,7 +53,7 @@ export const columns: VxeGridProps['columns'] = [
{
// 列标题
title: '设备类型 1边缘服务,2摄像头 3 热成像',
title: '设备类型:1边缘服务,2摄像头 3 热成像',
// 对应字段
field: 'deviceType',
@ -75,7 +75,7 @@ export const columns: VxeGridProps['columns'] = [
{
// 列标题
title: '在线状态0在线1不在线',
title: '在线状态:0在线1不在线',
// 对应字段
field: 'deviceOnline',
@ -108,7 +108,7 @@ export const columns: VxeGridProps['columns'] = [
{
// 列标题
title: '设备开关 0 开 1关 2停用',
title: '设备开关: 0 开 1关 2停用',
// 对应字段
field: 'deviceSwitch',
@ -163,7 +163,7 @@ export const columns: VxeGridProps['columns'] = [
{
// 列标题
title: '删除 0 默认 1删除',
title: '删除: 0 默认 1删除',
// 对应字段
field: 'deletedFlag',
@ -273,7 +273,7 @@ export const columns: VxeGridProps['columns'] = [
{
// 列标题
title: '0默认未绑定,1绑定',
title: '是否绑定: 0未绑定,1绑定',
// 对应字段
field: 'deviceBind',

10
vue-vben-admin/apps/web-antd/src/views/device/form.ts

@ -61,7 +61,7 @@ export const formSchema: VbenFormSchema[] = [
fieldName: 'deviceType',
// label
label: '设备类型 1边缘服务,2摄像头 3 热成像',
label: '设备类型:1边缘服务,2摄像头 3 热成像',
// 自动组件
component: 'InputNumber'
@ -85,7 +85,7 @@ export const formSchema: VbenFormSchema[] = [
fieldName: 'deviceOnline',
// label
label: '在线状态0在线1不在线',
label: '在线状态:0在线1不在线',
// 自动组件
component: 'InputNumber'
@ -121,7 +121,7 @@ export const formSchema: VbenFormSchema[] = [
fieldName: 'deviceSwitch',
// label
label: '设备开关 0 开 1关 2停用',
label: '设备开关: 0 开 1关 2停用',
// 自动组件
component: 'InputNumber'
@ -181,7 +181,7 @@ export const formSchema: VbenFormSchema[] = [
fieldName: 'deletedFlag',
// label
label: '删除 0 默认 1删除',
label: '删除: 0 默认 1删除',
// 自动组件
component: 'InputNumber'
@ -301,7 +301,7 @@ export const formSchema: VbenFormSchema[] = [
fieldName: 'deviceBind',
// label
label: '0默认未绑定,1绑定',
label: '是否绑定: 0未绑定,1绑定',
// 自动组件
component: 'InputNumber'

634
vue-vben-admin/apps/web-antd/src/views/device/index.vue

@ -1,20 +1,29 @@
<script setup lang="ts">
/**
* 用户管理页面
* device管理页面
*/
import {ref, reactive, onMounted, watch} from 'vue';
import {ref, reactive, onMounted, watch, h} from 'vue';
import {useVbenVxeGrid} from '#/adapter/vxe-table';
import {useVbenModal} from '@vben/common-ui';
import {useVbenForm} from '#/adapter/form';
import {Upload, message, Image} from 'ant-design-vue';
import dayjs from 'dayjs';
import {columns} from './data';
import {formSchema} from './form';
import { deviceApi } from '#/api/device';
// ========== API URL ==========
const API_BASE_URL = import.meta.env.VITE_GLOB_API_URL || '';
// ========== ==========
const currentRow = ref(null);
const isEdit = ref(false);
const modalTitle = ref('新增');
const uploadVisible = ref(false);
const uploadFieldName = ref('');
const uploadImageUrl = ref('');
const uploadedUrl = ref(''); //
const formUploadUrls = ref<Record<string, string>>({}); // URL
// ========== Schema ==========
const querySchema = ref([]);
// ========== ==========
@ -23,7 +32,14 @@ const enumData = reactive({
list: [], //
});
//todo
const hiddenColumns = ref(['fun_code','graduallyIntervalTime','funMsgTitle','funImg','createdAt', 'updatedBy', 'updatedAt', 'userPassword', 'userId', 'userSys', 'deletedFlag', "userFace"])
const hiddenColumns = ref(['fun_code', 'graduallyIntervalTime', 'funMsgTitle', 'funImg', 'createdAt', 'updatedBy', 'updatedAt', 'userPassword', 'userId', 'userSys', 'deletedFlag', "userFace"])
const editFields = ['userId', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy', 'deletedFlag'];
// ========== ==========
function isImageField(fieldName: string): boolean {
const lowerName = fieldName.toLowerCase();
return lowerName.includes('img') || lowerName.includes('face') || lowerName.includes('picture');
}
// ========== ==========
async function initEnumData() {
@ -118,10 +134,146 @@ watch(
{immediate: false}
);
// ========== ==========
// ========== ==========
const gridOptions = {
columns: [
...columns,
...columns.map(col => {
// ( img, face, picture)使 +
if (isImageField(col.field)) {
return {
...col,
width: 200,
align: 'center',
slots: {
default: ({row}: any) => {
const imgUrl = row[col.field];
//
if (!imgUrl) {
return h('div', {
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '8px',
height: '50px',
}
}, [
h('span', {
style: {
color: '#999',
fontSize: '12px'
}
}, '无图片'),
h('button', {
onClick: (e: Event) => {
e.stopPropagation();
//
currentRow.value = row;
isEdit.value = true; //
openUploadDialog(col.field, imgUrl);
},
style: {
padding: '4px 8px',
fontSize: '12px',
color: '#1890ff',
backgroundColor: '#e6f7ff',
border: '1px solid #1890ff',
borderRadius: '4px',
cursor: 'pointer',
}
}, '上传')
]);
}
// URL
let fullUrl = imgUrl;
if (!imgUrl.startsWith('http://') && !imgUrl.startsWith('https://')) {
// URL API
const apiURL = import.meta.env.VITE_GLOB_API_URL || '';
fullUrl = `${apiURL}${imgUrl}`;
}
console.log('🖼️ 图片 URL:', fullUrl);
//
try {
return h('div', {
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '8px',
}
}, [
h(Image, {
src: fullUrl,
height: 60,
width: 80,
preview: true,
style: {
borderRadius: '4px',
objectFit: 'cover',
cursor: 'pointer',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
},
onError: () => {
console.error('❌ 图片加载失败:', fullUrl);
return h('div', {
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '50px',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
color: '#999',
fontSize: '12px'
}
}, '加载失败');
}
}),
h('button', {
onClick: (e: Event) => {
e.stopPropagation();
//
currentRow.value = row;
isEdit.value = true; //
openUploadDialog(col.field, imgUrl);
},
style: {
padding: '4px 8px',
fontSize: '12px',
color: '#1890ff',
backgroundColor: '#e6f7ff',
border: '1px solid #1890ff',
borderRadius: '4px',
cursor: 'pointer',
}
}, '更换')
]);
} catch (error) {
console.error('❌ 图片渲染错误:', error);
return h('div', {
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '50px',
backgroundColor: '#fffbe6',
borderRadius: '4px',
color: '#faad14',
fontSize: '12px'
}
}, '渲染失败');
}
}
}
};
}
return col;
}),
{
field: 'action',
title: '操作',
@ -157,7 +309,7 @@ const gridOptions = {
}
} catch (error) {
console.error('查询用户列表失败:', error)
console.error('查询列表失败:', error)
throw error
}
}
@ -171,10 +323,15 @@ const gridOptions = {
enabled: true,
pageSize: 10,
},
showOverflow: true,
minHeight: '100%',
maxHeight: 'auto',
showHeaderOverflow: true,
}
const [Grid, gridApi] = useVbenVxeGrid({gridOptions})
// ========== Schema ==========
const editFormSchema = formSchema.filter(item => !editFields.includes(item.fieldName));
// ========== ==========
const [Modal, modalApi] = useVbenModal({
centered: true,
@ -190,20 +347,30 @@ const [Modal, modalApi] = useVbenModal({
const values = await formApi.validateAndSubmitForm();
if (!values) return;
// URL
const submitValues = !isEdit.value
? {...values, ...formUploadUrls.value}
: values;
// Day.js
const submitValues = {
...values,
userBirthday: values.userBirthday ? dayjs(values.userBirthday).format('YYYY-MM-DD') : null,
const finalSubmitValues = {
...submitValues,
userBirthday: submitValues.userBirthday ? dayjs(submitValues.userBirthday).format('YYYY-MM-DD') : null,
};
if (isEdit.value && currentRow.value?.id) {
await deviceApi.save({...submitValues, id: currentRow.value.id});
await deviceApi.save({...finalSubmitValues, id: currentRow.value.id});
} else {
await deviceApi.save(submitValues);
await deviceApi.add(finalSubmitValues);
}
modalApi.close();
gridApi.reload();
// URL
if (!isEdit.value) {
formUploadUrls.value = {};
}
} catch (error) {
console.error('保存失败:', error);
}
@ -211,13 +378,169 @@ const [Modal, modalApi] = useVbenModal({
onOpenChange(isOpen: boolean) {
if (!isOpen && !isEdit.value) {
formApi.resetForm();
formUploadUrls.value = {};
}
},
})
// ========== ==========
const uploadFileList = ref<any[]>([]);
const [UploadModal, uploadModalApi] = useVbenModal({
centered: true,
closable: true,
maskClosable: false,
draggable: true,
width: 600,
title: '上传图片',
onCancel() {
uploadModalApi.close();
uploadFileList.value = [];
uploadedUrl.value = '';
},
onConfirm: async () => {
console.log('=== 点击确认保存 ===', {
uploadedUrl: uploadedUrl.value,
isEdit: isEdit.value,
currentRowId: currentRow.value?.id,
uploadFieldName: uploadFieldName.value,
currentRowData: currentRow.value,
});
// URL
if (uploadedUrl.value) {
try {
// URL
if (currentRow.value) {
currentRow.value[uploadFieldName.value] = uploadedUrl.value;
console.log('✅ 已更新当前行图片字段:', {
fieldName: uploadFieldName.value,
newUrl: uploadedUrl.value,
updatedRow: currentRow.value,
});
}
// save
if (isEdit.value && currentRow.value?.id) {
const submitData = {
...currentRow.value,
id: currentRow.value.id
};
console.log('📤 提交保存数据:', submitData);
await deviceApi.save(submitData);
message.success('保存成功!');
//
uploadModalApi.close();
gridApi.reload();
//
uploadFileList.value = [];
uploadedUrl.value = '';
} else {
console.warn('⚠️ 不满足保存条件:', {
isEdit: isEdit.value,
hasId: !!currentRow.value?.id,
});
message.warning('非编辑模式或无 ID,仅更新预览');
//
uploadModalApi.close();
gridApi.reload();
}
} catch (error: any) {
console.error('❌ 保存失败:', error);
message.error(error?.message || '保存失败');
}
} else {
message.warning('请先选择并上传图片');
}
},
})
// ========== Schema ==========
// id
// id
const getEditFormSchema = () => {
return formSchema.filter(item => {
// editFields
if (!editFields.includes(item.fieldName)) {
// id
if (!isEdit.value && item.fieldName === 'id') {
return false;
}
return true;
}
return false;
}).map(item => {
//
if (!isEdit.value && isImageField(item.fieldName)) {
// 使 formUploadUrls URL使
const currentImageUrl = formUploadUrls.value[item.fieldName] || formApi.getValues()?.[item.fieldName] || '';
return {
...item,
component: 'Upload',
componentProps: {
class: 'w-full',
accept: 'image/*',
maxCount: 1,
listType: 'picture-card',
fileList: currentImageUrl && typeof currentImageUrl === 'string' ? [{
uid: '-1',
name: 'image.png',
status: 'done',
url: currentImageUrl,
}] : (Array.isArray(currentImageUrl) ? currentImageUrl : []),
beforeUpload: async (file: any) => {
console.log('📦 新增表单内上传文件:', file);
try {
const formData = new FormData();
formData.append('file', file);
// API
const res = await deviceApi.upload(formData);
const resultUrl = res.result || res.message || res.url || res;
console.log(item.fieldName + ' 新增表单内上传成功,URL:', resultUrl);
message.success('上传成功!');
// URL
formUploadUrls.value[item.fieldName] = resultUrl;
//
const currentValues = formApi.getValues();
formApi.setValues({
...currentValues,
[item.fieldName]: resultUrl
});
// schema fileList
setTimeout(() => {
formApi.setState({
schema: getEditFormSchema(),
});
}, 0);
formApi.resetForm();
// false
return false;
} catch (error: any) {
console.error('❌ 新增表单内上传失败:', error);
message.error(error?.message || '上传失败');
return false;
}
},
},
};
}
return item;
});
};
// ========== ==========
const [Form, formApi] = useVbenForm({
schema: formSchema,
schema: editFormSchema,
showDefaultActions: false,
wrapperClass: 'grid-cols-1 md:grid-cols-2',
commonConfig: {
@ -228,116 +551,114 @@ const [Form, formApi] = useVbenForm({
},
})
// ========== ==========
function handleAdd() {
isEdit.value = false;
modalTitle.value = '新增用户';
formApi.resetForm();
modalApi.open();
}
// ========== ==========
function handleEdit(row: any) {
isEdit.value = true;
currentRow.value = row;
modalTitle.value = '编辑用户';
//
const formValues = {
//
id: row.id,
//
deviceName: row.deviceName,
//
deviceBrand: row.deviceBrand,
//
deviceModel: row.deviceModel,
//
deviceType: row.deviceType,
//
deviceOrder: row.deviceOrder,
//
deviceOnline: row.deviceOnline,
//
roomId: row.roomId,
//
roomName: row.roomName,
//
deviceSwitch: row.deviceSwitch,
//
createdAt: row.createdAt,
//
createdBy: row.createdBy,
//
updatedAt: row.updatedAt,
//
updatedBy: row.updatedBy,
//
deletedFlag: row.deletedFlag,
//
userId: row.userId,
//
classId: row.classId,
// ========== ==========
function openUploadDialog(fieldName: string, currentUrl: string) {
console.log('=== 打开上传对话框 ===', {
fieldName,
currentUrl,
isEdit: isEdit.value,
currentRowId: currentRow.value?.id,
currentRowFull: currentRow.value,
});
//
deviceUrl: row.deviceUrl,
uploadFieldName.value = fieldName;
uploadImageUrl.value = currentUrl || '';
uploadedUrl.value = ''; // URL
uploadFileList.value = []; //
uploadVisible.value = true;
uploadModalApi.open();
}
//
deviceCode: row.deviceCode,
// ========== - 使 custom-request ==========
async function handleCustomRequest(options: any) {
const {file, onSuccess, onError} = options;
//
deviceLanUser: row.deviceLanUser,
try {
console.log('📦 开始上传文件:', file);
console.log('📦 文件详情:', {
name: file.name,
type: file.type,
size: file.size,
lastModified: file.lastModified,
});
//
deviceLanPassword: row.deviceLanPassword,
const formData = new FormData();
formData.append('file', file);
//
deviceLanIp: row.deviceLanIp,
// API
const res = await deviceApi.upload(formData);
const resultUrl = res.result || res.message || res.url || res;
//
deviceNote: row.deviceNote,
console.log('✅ 上传成功,URL:', resultUrl);
message.success('上传成功!');
//
deviceAddressIp: row.deviceAddressIp,
// URL
uploadedUrl.value = resultUrl;
console.log('=== uploadedUrl 赋值后 ===', uploadedUrl.value);
//
deviceBind: row.deviceBind,
//
uploadFileList.value = [{
uid: file.uid,
name: file.name,
status: 'done',
url: resultUrl,
}];
//
deviceImg: row.deviceImg,
//
onSuccess({url: resultUrl});
//
warnImg: row.warnImg,
} catch (error: any) {
console.error('❌ 上传失败:', error);
message.error(error?.message || '上传失败');
onError(error);
}
}
//
familyId: row.familyId,
// ==========
function handleChange(info: any) {
const {status} = info.file;
console.log('📋 文件状态变化:', status, info.file);
//
serverVersion: row.serverVersion,
if (status === 'done') {
console.log('✅ 上传完成响应:', info);
} else if (status === 'error') {
console.error('❌ 上传失败:', info);
message.error(`${info.file.name} 上传失败`);
}
}
//
aiVersion: row.aiVersion,
//
upSwitch: row.upSwitch,
// ========== ==========
function handleAdd() {
isEdit.value = false;
modalTitle.value = '功能配置表新增';
// URL
formUploadUrls.value = {};
// schema ( id)
formApi.setState({
schema: getEditFormSchema(),
});
formApi.resetForm();
modalApi.open();
}
};
// ========== ==========
function handleEdit(row: any) {
isEdit.value = true;
currentRow.value = row;
modalTitle.value = '功能配置表编辑';
// schema ( id)
formApi.setState({
schema: getEditFormSchema(),
});
// editFields
const formValues: any = {};
editFormSchema.forEach(item => {
const field = item.fieldName;
if (row[field] !== undefined && row[field] !== null) {
formValues[field] = row[field];
}
});
formApi.setValues(formValues);
modalApi.open();
@ -367,12 +688,17 @@ function handleReset() {
</script>
<template>
<div style="height: 100vh; padding: 16px; box-sizing: border-box;">
<div
style="height: 100vh; padding: 16px; box-sizing: border-box; display: flex; flex-direction: column;">
<!-- 查询表单 -->
<div class="bg-card mb-4 p-4 rounded shadow">
<div class="bg-card mb-4 p-4 rounded shadow flex-shrink-0">
<h3 class="text-lg font-semibold mb-3">查询条件</h3>
<QueryForm/>
<div v-if="enumData.loading" class="text-center py-4 text-gray-500">
正在加载查询条件...
</div>
<QueryForm v-else/>
<div class="mt-3 flex gap-2">
<button @click="handleSearch"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
@ -385,25 +711,81 @@ function handleReset() {
</div>
</div>
<!-- 数据表格 -->
<Grid>
<template #toolbar-tools>
<button @click="handleAdd" class="mr-2"> 新增</button>
<button @click="() => gridApi.reload()">🔄 刷新</button>
</template>
<template #action="{ row }">
<div class="flex gap-2">
<button @click="() => handleEdit(row)" class="text-blue-500 hover:text-blue-700"> 编辑
</button>
<button @click="() => handleDelete(row)" class="text-red-500 hover:text-red-700">🗑 删除
</button>
</div>
</template>
</Grid>
<!-- 数据表格 (可滚动区域) -->
<div class="flex-1 overflow-hidden bg-card rounded shadow">
<Grid>
<template #toolbar-tools>
<button @click="handleAdd" class="mr-2"> 新增</button>
<button @click="() => gridApi.reload()">🔄 刷新</button>
</template>
<template #action="{ row }">
<div class="flex gap-2">
<button @click="() => handleEdit(row)" class="text-blue-500 hover:text-blue-700"> 编辑
</button>
<button @click="() => handleDelete(row)" class="text-red-500 hover:text-red-700">🗑 删除
</button>
</div>
</template>
</Grid>
</div>
<Modal :title="modalTitle">
<Form/>
</Modal>
<!-- 上传对话框 -->
<!-- 上传对话框 -->
<UploadModal>
<div style="padding: 20px;margin-top: 16px; text-align: center;">
<Upload
name="file"
:file-list="uploadFileList"
:custom-request="handleCustomRequest"
:show-upload-list="true"
accept="image/*" style="width: 100%;"
>
<div
style="width: 100%; height: 150px; border: 2px dashed #d9d9d9; border-radius: 8px; background: #fafafa; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer;">
<!-- 使用 SVG 图标代替 -->
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#1890ff"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="margin-bottom: 8px;">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<span style="color: #666;">点击或拖拽图片到此上传</span>
<span style="color: #999; font-size: 12px; margin-top: 4px;">支持 JPGPNG 格式</span>
</div>
</Upload>
<!-- 展示上传成功后的图片 -->
<div v-if="uploadedUrl" style="margin-top: 16px; text-align: center;">
<p style="color: #67c23a; font-size: 14px; margin-bottom: 8px;"> 上传成功</p>
<p style="color: #999; font-size: 12px; margin-bottom: 8px;">新图片预览:</p>
<Image
:src="uploadedUrl"
style="max-width: 200px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"
:preview="true"
/>
<p style="color: #999; font-size: 12px; margin-top: 8px; word-break: break-all;"></p>
</div>
<!-- 展示原图如果有 -->
<div v-if="uploadImageUrl && !uploadedUrl" style="margin-top: 16px; text-align: center;">
<p style="color: #999; font-size: 12px; margin-bottom: 8px;">当前图片:</p>
<Image
:src="uploadImageUrl.startsWith('http') || uploadImageUrl.startsWith('/') ? uploadImageUrl : `${API_BASE_URL}${uploadImageUrl}`"
style="max-width: 200px; border-radius: 4px;"
:preview="true"
/>
</div>
</div>
</UploadModal>
</div>
</template>
<style scoped>
:deep(.vxe-pager--wrapper) {
margin-bottom: 0.5rem;
}
</style>

178
vue-vben-admin/apps/web-antd/src/views/enums/data.ts

@ -2,131 +2,131 @@
*
*/
import type { VxeGridProps } from '#/adapter/vxe-table';
import type {VxeGridProps} from '#/adapter/vxe-table';
export const columns: VxeGridProps['columns'] = [
{
// 列标题
title: 'ID',
{
// 列标题
title: 'ID',
// 对应字段
field: 'id',
// 对应字段
field: 'id',
// 宽度
width: 150
},
// 宽度
width: 150
},
{
// 列标题
title: '字段名',
{
// 列标题
title: '字段名',
// 对应字段
field: 'fieldName',
// 对应字段
field: 'fieldName',
// 宽度
width: 150
},
// 宽度
width: 150
},
{
// 列标题
title: '值说明',
{
// 列标题
title: '值说明',
// 对应字段
field: 'label',
// 对应字段
field: 'label',
// 宽度
width: 150
},
// 宽度
width: 150
},
{
// 列标题
title: '值',
{
// 列标题
title: '值',
// 对应字段
field: 'value',
// 对应字段
field: 'value',
// 宽度
width: 150
},
// 宽度
width: 150
},
{
// 列标题
title: '字段说明',
{
// 列标题
title: '字段说明',
// 对应字段
field: 'fieldLabel',
// 对应字段
field: 'fieldLabel',
// 宽度
width: 150
},
// 宽度
width: 150
},
{
// 列标题
title: '创建时间',
{
// 列标题
title: '创建时间',
// 对应字段
field: 'createdAt',
// 对应字段
field: 'createdAt',
// 宽度
width: 150
},
// 宽度
width: 150
},
{
// 列标题
title: '更新时间',
{
// 列标题
title: '更新时间',
// 对应字段
field: 'updatedAt',
// 对应字段
field: 'updatedAt',
// 宽度
width: 150
},
// 宽度
width: 150
},
{
// 列标题
title: '创建人员',
{
// 列标题
title: '创建人员',
// 对应字段
field: 'createdBy',
// 对应字段
field: 'createdBy',
// 宽度
width: 150
},
// 宽度
width: 150
},
{
// 列标题
title: '更新人',
{
// 列标题
title: '更新人',
// 对应字段
field: 'updatedBy',
// 对应字段
field: 'updatedBy',
// 宽度
width: 150
},
// 宽度
width: 150
},
{
// 列标题
title: '是否删除',
{
// 列标题
title: '是否删除',
// 对应字段
field: 'deletedFlag',
// 对应字段
field: 'deletedFlag',
// 宽度
width: 150
},
// 宽度
width: 150
},
{
// 列标题
title: '冗余字段',
{
// 列标题
title: '冗余字段',
// 对应字段
field: 'userId',
// 对应字段
field: 'userId',
// 宽度
width: 150
},
// 宽度
width: 150
},
];

6
vue-vben-admin/apps/web-antd/src/views/fun/data.ts

@ -64,7 +64,7 @@ export const columns: VxeGridProps['columns'] = [
{
// 列标题
title: '告警基本0默认,1级,2级,3级',
title: '告警级别: 0默认,1级,2级,3级',
// 对应字段
field: 'funWarnLevel',
@ -86,7 +86,7 @@ export const columns: VxeGridProps['columns'] = [
{
// 列标题
title: '是否在首页展示0展示,1不展示',
title: '是否在首页展示: 0展示,1不展示',
// 对应字段
field: 'funIndex',
@ -196,7 +196,7 @@ export const columns: VxeGridProps['columns'] = [
{
// 列标题
title: '默认0开通,1关闭,2影藏',
title: '功能状态: 默认 0开通,1关闭,2隐藏',
// 对应字段
field: 'funStatus',

6
vue-vben-admin/apps/web-antd/src/views/fun/form.ts

@ -73,7 +73,7 @@ export const formSchema: VbenFormSchema[] = [
fieldName: 'funWarnLevel',
// label
label: '告警基本0默认,1级,2级,3级',
label: '告警级别: 0默认,1级,2级,3级',
// 自动组件
component: 'InputNumber'
@ -97,7 +97,7 @@ export const formSchema: VbenFormSchema[] = [
fieldName: 'funIndex',
// label
label: '是否在首页展示0展示,1不展示',
label: '是否在首页展示: 0展示,1不展示',
// 自动组件
component: 'InputNumber'
@ -217,7 +217,7 @@ export const formSchema: VbenFormSchema[] = [
fieldName: 'funStatus',
// label
label: '默认0开通,1关闭,2影藏',
label: '功能状态: 默认 0开通,1关闭,2隐藏',
// 自动组件
component: 'InputNumber'

514
vue-vben-admin/apps/web-antd/src/views/fun/index.vue

@ -2,19 +2,28 @@
/**
* fun管理页面
*/
import {ref, reactive, onMounted, watch} from 'vue';
import {ref, reactive, onMounted, watch, h} from 'vue';
import {useVbenVxeGrid} from '#/adapter/vxe-table';
import {useVbenModal} from '@vben/common-ui';
import {useVbenForm} from '#/adapter/form';
import {Upload, message, Image} from 'ant-design-vue';
import dayjs from 'dayjs';
import {columns} from './data';
import {formSchema} from './form';
import { funApi } from '#/api/fun';
// ========== API URL ==========
const API_BASE_URL = import.meta.env.VITE_GLOB_API_URL || '';
// ========== ==========
const currentRow = ref(null);
const isEdit = ref(false);
const modalTitle = ref('新增');
const uploadVisible = ref(false);
const uploadFieldName = ref('');
const uploadImageUrl = ref('');
const uploadedUrl = ref(''); //
const formUploadUrls = ref<Record<string, string>>({}); // URL
// ========== Schema ==========
const querySchema = ref([]);
// ========== ==========
@ -23,8 +32,14 @@ const enumData = reactive({
list: [], //
});
//todo
const hiddenColumns = ref(['fun_code','graduallyIntervalTime','funMsgTitle','funImg','createdAt', 'updatedBy', 'updatedAt', 'userPassword', 'userId', 'userSys', 'deletedFlag', "userFace"])
const editFields = ['userId','createdAt','createdBy','updatedAt','updatedBy','deletedFlag'];
const hiddenColumns = ref(['fun_code', 'graduallyIntervalTime', 'funMsgTitle', 'funImg', 'createdAt', 'updatedBy', 'updatedAt', 'userPassword', 'userId', 'userSys', 'deletedFlag', "userFace"])
const editFields = ['userId', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy', 'deletedFlag'];
// ========== ==========
function isImageField(fieldName: string): boolean {
const lowerName = fieldName.toLowerCase();
return lowerName.includes('img') || lowerName.includes('face') || lowerName.includes('picture');
}
// ========== ==========
async function initEnumData() {
@ -119,10 +134,146 @@ watch(
{immediate: false}
);
// ========== ==========
// ========== ==========
const gridOptions = {
columns: [
...columns,
...columns.map(col => {
// ( img, face, picture)使 +
if (isImageField(col.field)) {
return {
...col,
width: 200,
align: 'center',
slots: {
default: ({row}: any) => {
const imgUrl = row[col.field];
//
if (!imgUrl) {
return h('div', {
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '8px',
height: '50px',
}
}, [
h('span', {
style: {
color: '#999',
fontSize: '12px'
}
}, '无图片'),
h('button', {
onClick: (e: Event) => {
e.stopPropagation();
//
currentRow.value = row;
isEdit.value = true; //
openUploadDialog(col.field, imgUrl);
},
style: {
padding: '4px 8px',
fontSize: '12px',
color: '#1890ff',
backgroundColor: '#e6f7ff',
border: '1px solid #1890ff',
borderRadius: '4px',
cursor: 'pointer',
}
}, '上传')
]);
}
// URL
let fullUrl = imgUrl;
if (!imgUrl.startsWith('http://') && !imgUrl.startsWith('https://')) {
// URL API
const apiURL = import.meta.env.VITE_GLOB_API_URL || '';
fullUrl = `${apiURL}${imgUrl}`;
}
console.log('🖼️ 图片 URL:', fullUrl);
//
try {
return h('div', {
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '8px',
}
}, [
h(Image, {
src: fullUrl,
height: 60,
width: 80,
preview: true,
style: {
borderRadius: '4px',
objectFit: 'cover',
cursor: 'pointer',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
},
onError: () => {
console.error('❌ 图片加载失败:', fullUrl);
return h('div', {
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '50px',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
color: '#999',
fontSize: '12px'
}
}, '加载失败');
}
}),
h('button', {
onClick: (e: Event) => {
e.stopPropagation();
//
currentRow.value = row;
isEdit.value = true; //
openUploadDialog(col.field, imgUrl);
},
style: {
padding: '4px 8px',
fontSize: '12px',
color: '#1890ff',
backgroundColor: '#e6f7ff',
border: '1px solid #1890ff',
borderRadius: '4px',
cursor: 'pointer',
}
}, '更换')
]);
} catch (error) {
console.error('❌ 图片渲染错误:', error);
return h('div', {
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '50px',
backgroundColor: '#fffbe6',
borderRadius: '4px',
color: '#faad14',
fontSize: '12px'
}
}, '渲染失败');
}
}
}
};
}
return col;
}),
{
field: 'action',
title: '操作',
@ -172,6 +323,10 @@ const gridOptions = {
enabled: true,
pageSize: 10,
},
showOverflow: true,
minHeight: '100%',
maxHeight: 'auto',
showHeaderOverflow: true,
}
const [Grid, gridApi] = useVbenVxeGrid({gridOptions})
@ -192,20 +347,30 @@ const [Modal, modalApi] = useVbenModal({
const values = await formApi.validateAndSubmitForm();
if (!values) return;
// URL
const submitValues = !isEdit.value
? {...values, ...formUploadUrls.value}
: values;
// Day.js
const submitValues = {
...values,
userBirthday: values.userBirthday ? dayjs(values.userBirthday).format('YYYY-MM-DD') : null,
const finalSubmitValues = {
...submitValues,
userBirthday: submitValues.userBirthday ? dayjs(submitValues.userBirthday).format('YYYY-MM-DD') : null,
};
if (isEdit.value && currentRow.value?.id) {
await funApi.save({...submitValues, id: currentRow.value.id});
await funApi.save({...finalSubmitValues, id: currentRow.value.id});
} else {
await funApi.add(submitValues);
await funApi.add(finalSubmitValues);
}
modalApi.close();
gridApi.reload();
// URL
if (!isEdit.value) {
formUploadUrls.value = {};
}
} catch (error) {
console.error('保存失败:', error);
}
@ -213,12 +378,90 @@ const [Modal, modalApi] = useVbenModal({
onOpenChange(isOpen: boolean) {
if (!isOpen && !isEdit.value) {
formApi.resetForm();
formUploadUrls.value = {};
}
},
})
// ========== ==========
const uploadFileList = ref<any[]>([]);
const [UploadModal, uploadModalApi] = useVbenModal({
centered: true,
closable: true,
maskClosable: false,
draggable: true,
width: 600,
title: '上传图片',
onCancel() {
uploadModalApi.close();
uploadFileList.value = [];
uploadedUrl.value = '';
},
onConfirm: async () => {
console.log('=== 点击确认保存 ===', {
uploadedUrl: uploadedUrl.value,
isEdit: isEdit.value,
currentRowId: currentRow.value?.id,
uploadFieldName: uploadFieldName.value,
currentRowData: currentRow.value,
});
// URL
if (uploadedUrl.value) {
try {
// URL
if (currentRow.value) {
currentRow.value[uploadFieldName.value] = uploadedUrl.value;
console.log('✅ 已更新当前行图片字段:', {
fieldName: uploadFieldName.value,
newUrl: uploadedUrl.value,
updatedRow: currentRow.value,
});
}
// save
if (isEdit.value && currentRow.value?.id) {
const submitData = {
...currentRow.value,
id: currentRow.value.id
};
console.log('📤 提交保存数据:', submitData);
await funApi.save(submitData);
message.success('保存成功!');
//
uploadModalApi.close();
gridApi.reload();
//
uploadFileList.value = [];
uploadedUrl.value = '';
} else {
console.warn('⚠️ 不满足保存条件:', {
isEdit: isEdit.value,
hasId: !!currentRow.value?.id,
});
message.warning('非编辑模式或无 ID,仅更新预览');
//
uploadModalApi.close();
gridApi.reload();
}
} catch (error: any) {
console.error('❌ 保存失败:', error);
message.error(error?.message || '保存失败');
}
} else {
message.warning('请先选择并上传图片');
}
},
})
// ========== Schema ==========
// id
// id
// id
const getEditFormSchema = () => {
return formSchema.filter(item => {
@ -231,9 +474,64 @@ const getEditFormSchema = () => {
return true;
}
return false;
}).map(item => {
//
if (!isEdit.value && isImageField(item.fieldName)) {
// 使 formUploadUrls URL使
const currentImageUrl = formUploadUrls.value[item.fieldName] || '';
return {
...item,
component: 'Upload',
componentProps: {
class: 'w-full',
accept: 'image/*',
maxCount: 1,
listType: 'picture-card',
fileList: currentImageUrl && typeof currentImageUrl === 'string' ? [{
uid: '-1',
name: 'image.png',
status: 'done',
url: currentImageUrl,
}] : (Array.isArray(currentImageUrl) ? currentImageUrl : []),
beforeUpload: async (file: any) => {
console.log('📦 新增表单内上传文件:', file);
try {
const formData = new FormData();
formData.append('file', file);
// API
const res = await funApi.upload(formData);
const resultUrl = res.result || res.message || res.url || res;
console.log(item.fieldName + ' 新增表单内上传成功,URL:', resultUrl);
message.success('上传成功!');
// URL
formUploadUrls.value[item.fieldName] = resultUrl;
// schema fileList
setTimeout(() => {
formApi.setState({
schema: getEditFormSchema(),
});
}, 0);
// Deleted:formApi.resetForm();
// false
return false;
} catch (error: any) {
console.error('❌ 新增表单内上传失败:', error);
message.error(error?.message || '上传失败');
return false;
}
},
},
};
}
return item;
});
};
// ========== ==========
const [Form, formApi] = useVbenForm({
schema: editFormSchema,
@ -247,12 +545,89 @@ const [Form, formApi] = useVbenForm({
},
})
// ========== ==========
function openUploadDialog(fieldName: string, currentUrl: string) {
console.log('=== 打开上传对话框 ===', {
fieldName,
currentUrl,
isEdit: isEdit.value,
currentRowId: currentRow.value?.id,
currentRowFull: currentRow.value,
});
uploadFieldName.value = fieldName;
uploadImageUrl.value = currentUrl || '';
uploadedUrl.value = ''; // URL
uploadFileList.value = []; //
uploadVisible.value = true;
uploadModalApi.open();
}
// ========== - 使 custom-request ==========
async function handleCustomRequest(options: any) {
const {file, onSuccess, onError} = options;
try {
console.log('📦 开始上传文件:', file);
console.log('📦 文件详情:', {
name: file.name,
type: file.type,
size: file.size,
lastModified: file.lastModified,
});
const formData = new FormData();
formData.append('file', file);
// API
const res = await funApi.upload(formData);
const resultUrl = res.result || res.message || res.url || res;
console.log('✅ 上传成功,URL:', resultUrl);
message.success('上传成功!');
// URL
uploadedUrl.value = resultUrl;
console.log('=== uploadedUrl 赋值后 ===', uploadedUrl.value);
//
uploadFileList.value = [{
uid: file.uid,
name: file.name,
status: 'done',
url: resultUrl,
}];
//
onSuccess({url: resultUrl});
} catch (error: any) {
console.error('❌ 上传失败:', error);
message.error(error?.message || '上传失败');
onError(error);
}
}
// ==========
function handleChange(info: any) {
const {status} = info.file;
console.log('📋 文件状态变化:', status, info.file);
if (status === 'done') {
console.log('✅ 上传完成响应:', info);
} else if (status === 'error') {
console.error('❌ 上传失败:', info);
message.error(`${info.file.name} 上传失败`);
}
}
// ========== ==========
function handleAdd() {
isEdit.value = false;
modalTitle.value = '功能配置新增';
modalTitle.value = '功能配置表新增';
// URL
formUploadUrls.value = {};
// schema ( id)
formApi.setState({
schema: getEditFormSchema(),
@ -265,19 +640,19 @@ function handleAdd() {
function handleEdit(row: any) {
isEdit.value = true;
currentRow.value = row;
modalTitle.value = '功能配置编辑';
modalTitle.value = '功能配置编辑';
// schema ( id)
formApi.setState({
schema: getEditFormSchema(),
});
// editFields
const formValues: any = {};
editFormSchema.forEach(item => {
const field = item.fieldName;
if (row[field] !== undefined && row[field] !== null) {
formValues[field] = row[field];
}
});
const formValues: any = {};
editFormSchema.forEach(item => {
const field = item.fieldName;
if (row[field] !== undefined && row[field] !== null) {
formValues[field] = row[field];
}
});
formApi.setValues(formValues);
modalApi.open();
@ -307,12 +682,17 @@ function handleReset() {
</script>
<template>
<div style="height: 100vh; padding: 16px; box-sizing: border-box;">
<div
style="height: 100vh; padding: 16px; box-sizing: border-box; display: flex; flex-direction: column;">
<!-- 查询表单 -->
<div class="bg-card mb-4 p-4 rounded shadow">
<div class="bg-card mb-4 p-4 rounded shadow flex-shrink-0">
<h3 class="text-lg font-semibold mb-3">查询条件</h3>
<QueryForm/>
<div v-if="enumData.loading" class="text-center py-4 text-gray-500">
正在加载查询条件...
</div>
<QueryForm v-else/>
<div class="mt-3 flex gap-2">
<button @click="handleSearch"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
@ -325,25 +705,81 @@ function handleReset() {
</div>
</div>
<!-- 数据表格 -->
<Grid>
<template #toolbar-tools>
<button @click="handleAdd" class="mr-2"> 新增</button>
<button @click="() => gridApi.reload()">🔄 刷新</button>
</template>
<template #action="{ row }">
<div class="flex gap-2">
<button @click="() => handleEdit(row)" class="text-blue-500 hover:text-blue-700"> 编辑
</button>
<button @click="() => handleDelete(row)" class="text-red-500 hover:text-red-700">🗑 删除
</button>
</div>
</template>
</Grid>
<!-- 数据表格 (可滚动区域) -->
<div class="flex-1 overflow-hidden bg-card rounded shadow">
<Grid>
<template #toolbar-tools>
<button @click="handleAdd" class="mr-2"> 新增</button>
<button @click="() => gridApi.reload()">🔄 刷新</button>
</template>
<template #action="{ row }">
<div class="flex gap-2">
<button @click="() => handleEdit(row)" class="text-blue-500 hover:text-blue-700"> 编辑
</button>
<button @click="() => handleDelete(row)" class="text-red-500 hover:text-red-700">🗑 删除
</button>
</div>
</template>
</Grid>
</div>
<Modal :title="modalTitle">
<Form/>
</Modal>
<!-- 上传对话框 -->
<!-- 上传对话框 -->
<UploadModal>
<div style="padding: 20px;margin-top: 16px; text-align: center;">
<Upload
name="file"
:file-list="uploadFileList"
:custom-request="handleCustomRequest"
:show-upload-list="true"
accept="image/*" style="width: 100%;"
>
<div
style="width: 100%; height: 150px; border: 2px dashed #d9d9d9; border-radius: 8px; background: #fafafa; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer;">
<!-- 使用 SVG 图标代替 -->
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#1890ff"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="margin-bottom: 8px;">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<span style="color: #666;">点击或拖拽图片到此上传</span>
<span style="color: #999; font-size: 12px; margin-top: 4px;">支持 JPGPNG 格式</span>
</div>
</Upload>
<!-- 展示上传成功后的图片 -->
<div v-if="uploadedUrl" style="margin-top: 16px; text-align: center;">
<p style="color: #67c23a; font-size: 14px; margin-bottom: 8px;"> 上传成功</p>
<p style="color: #999; font-size: 12px; margin-bottom: 8px;">新图片预览:</p>
<Image
:src="uploadedUrl"
style="max-width: 200px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"
:preview="true"
/>
<p style="color: #999; font-size: 12px; margin-top: 8px; word-break: break-all;"></p>
</div>
<!-- 展示原图如果有 -->
<div v-if="uploadImageUrl && !uploadedUrl" style="margin-top: 16px; text-align: center;">
<p style="color: #999; font-size: 12px; margin-bottom: 8px;">当前图片:</p>
<Image
:src="uploadImageUrl.startsWith('http') || uploadImageUrl.startsWith('/') ? uploadImageUrl : `${API_BASE_URL}${uploadImageUrl}`"
style="max-width: 200px; border-radius: 4px;"
:preview="true"
/>
</div>
</div>
</UploadModal>
</div>
</template>
<style scoped>
:deep(.vxe-pager--wrapper) {
margin-bottom: 0.5rem;
}
</style>

53
vue-vben-admin/apps/web-antd/src/views/user/index.vue

@ -172,6 +172,10 @@ const gridOptions = {
enabled: true,
pageSize: 10,
},
showOverflow: true,
minHeight: '100%',
maxHeight: 'auto',
showHeaderOverflow: true,
}
const [Grid, gridApi] = useVbenVxeGrid({gridOptions})
@ -307,12 +311,16 @@ function handleReset() {
</script>
<template>
<div style="height: 100vh; padding: 16px; box-sizing: border-box;">
<div style="height: 100vh; padding: 16px; box-sizing: border-box; display: flex; flex-direction: column;">
<!-- 查询表单 -->
<div class="bg-card mb-4 p-4 rounded shadow">
<div class="bg-card mb-4 p-4 rounded shadow flex-shrink-0">
<h3 class="text-lg font-semibold mb-3">查询条件</h3>
<QueryForm/>
<div v-if="enumData.loading" class="text-center py-4 text-gray-500">
正在加载查询条件...
</div>
<QueryForm v-else />
<div class="mt-3 flex gap-2">
<button @click="handleSearch"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
@ -325,25 +333,32 @@ function handleReset() {
</div>
</div>
<!-- 数据表格 -->
<Grid>
<template #toolbar-tools>
<button @click="handleAdd" class="mr-2"> 新增</button>
<button @click="() => gridApi.reload()">🔄 刷新</button>
</template>
<template #action="{ row }">
<div class="flex gap-2">
<button @click="() => handleEdit(row)" class="text-blue-500 hover:text-blue-700"> 编辑
</button>
<button @click="() => handleDelete(row)" class="text-red-500 hover:text-red-700">🗑 删除
</button>
</div>
</template>
</Grid>
<!-- 数据表格 (可滚动区域) -->
<div class="flex-1 overflow-hidden bg-card rounded shadow">
<Grid>
<template #toolbar-tools>
<button @click="handleAdd" class="mr-2"> 新增</button>
<button @click="() => gridApi.reload()">🔄 刷新</button>
</template>
<template #action="{ row }">
<div class="flex gap-2">
<button @click="() => handleEdit(row)" class="text-blue-500 hover:text-blue-700"> 编辑
</button>
<button @click="() => handleDelete(row)" class="text-red-500 hover:text-red-700">🗑 删除
</button>
</div>
</template>
</Grid>
</div>
<Modal :title="modalTitle">
<Form/>
</Modal>
</div>
</template>
<style scoped>
:deep(.vxe-pager--wrapper) {
margin-bottom: 0.5rem;
}
</style>

Loading…
Cancel
Save