diff --git a/api_generator_vue.py b/api_generator_vue.py new file mode 100644 index 0000000..139f128 --- /dev/null +++ b/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("❌ 未输入表名,退出程序") \ No newline at end of file diff --git a/templates/vue/index.vue.j2 b/templates/vue/index.vue.j2 index 79132fb..943fac4 100644 --- a/templates/vue/index.vue.j2 +++ b/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>({}); // 存储新增弹窗中各字段的上传 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%;" > -
+
- + @@ -685,17 +758,19 @@ function handleReset() {

✅ 上传成功!

新图片预览:

-

{{ uploadedUrl }}

+

当前图片:

diff --git a/vue-vben-admin/apps/web-antd/src/api/act_log.ts b/vue-vben-admin/apps/web-antd/src/api/act_log.ts index 0f06a95..437a849 100644 --- a/vue-vben-admin/apps/web-antd/src/api/act_log.ts +++ b/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'}}); } } \ No newline at end of file diff --git a/vue-vben-admin/apps/web-antd/src/api/device.ts b/vue-vben-admin/apps/web-antd/src/api/device.ts index ae46ab0..02c305c 100644 --- a/vue-vben-admin/apps/web-antd/src/api/device.ts +++ b/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'}}); + } + } \ No newline at end of file diff --git a/vue-vben-admin/apps/web-antd/src/api/fun.ts b/vue-vben-admin/apps/web-antd/src/api/fun.ts index 7f14aa1..63f3ea4 100644 --- a/vue-vben-admin/apps/web-antd/src/api/fun.ts +++ b/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'}}); + } + } \ No newline at end of file diff --git a/vue-vben-admin/apps/web-antd/src/api/user.ts b/vue-vben-admin/apps/web-antd/src/api/user.ts index b992bca..d8b8d7c 100644 --- a/vue-vben-admin/apps/web-antd/src/api/user.ts +++ b/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'}}); } -} +} \ No newline at end of file diff --git a/vue-vben-admin/apps/web-antd/src/router/routes/modules/fun.ts b/vue-vben-admin/apps/web-antd/src/router/routes/modules/fun.ts index 67ee808..5cc53a4 100644 --- a/vue-vben-admin/apps/web-antd/src/router/routes/modules/fun.ts +++ b/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', diff --git a/vue-vben-admin/apps/web-antd/src/views/act_log/index.vue b/vue-vben-admin/apps/web-antd/src/views/act_log/index.vue index d952743..62befeb 100644 --- a/vue-vben-admin/apps/web-antd/src/views/act_log/index.vue +++ b/vue-vben-admin/apps/web-antd/src/views/act_log/index.vue @@ -346,4 +346,4 @@ function handleReset() {
- \ No newline at end of file + diff --git a/vue-vben-admin/apps/web-antd/src/views/device/data.ts b/vue-vben-admin/apps/web-antd/src/views/device/data.ts index 706d0e9..b87e7f5 100644 --- a/vue-vben-admin/apps/web-antd/src/views/device/data.ts +++ b/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', diff --git a/vue-vben-admin/apps/web-antd/src/views/device/form.ts b/vue-vben-admin/apps/web-antd/src/views/device/form.ts index 13d7f20..3e22a81 100644 --- a/vue-vben-admin/apps/web-antd/src/views/device/form.ts +++ b/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' diff --git a/vue-vben-admin/apps/web-antd/src/views/device/index.vue b/vue-vben-admin/apps/web-antd/src/views/device/index.vue index bc49b67..3e0e17e 100644 --- a/vue-vben-admin/apps/web-antd/src/views/device/index.vue +++ b/vue-vben-admin/apps/web-antd/src/views/device/index.vue @@ -1,20 +1,29 @@ + \ No newline at end of file + + \ No newline at end of file diff --git a/vue-vben-admin/apps/web-antd/src/views/enums/data.ts b/vue-vben-admin/apps/web-antd/src/views/enums/data.ts index ee18cce..6ec978f 100644 --- a/vue-vben-admin/apps/web-antd/src/views/enums/data.ts +++ b/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 + }, -]; \ No newline at end of file +]; diff --git a/vue-vben-admin/apps/web-antd/src/views/fun/data.ts b/vue-vben-admin/apps/web-antd/src/views/fun/data.ts index d7d4156..898e714 100644 --- a/vue-vben-admin/apps/web-antd/src/views/fun/data.ts +++ b/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', diff --git a/vue-vben-admin/apps/web-antd/src/views/fun/form.ts b/vue-vben-admin/apps/web-antd/src/views/fun/form.ts index 1ea9963..20493cd 100644 --- a/vue-vben-admin/apps/web-antd/src/views/fun/form.ts +++ b/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' diff --git a/vue-vben-admin/apps/web-antd/src/views/fun/index.vue b/vue-vben-admin/apps/web-antd/src/views/fun/index.vue index b8a178c..324d114 100644 --- a/vue-vben-admin/apps/web-antd/src/views/fun/index.vue +++ b/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>({}); // 存储新增弹窗中各字段的上传 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([]); +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() { + \ No newline at end of file + + \ No newline at end of file diff --git a/vue-vben-admin/apps/web-antd/src/views/user/index.vue b/vue-vben-admin/apps/web-antd/src/views/user/index.vue index b2d417a..fccaf7f 100644 --- a/vue-vben-admin/apps/web-antd/src/views/user/index.vue +++ b/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() { + \ No newline at end of file + +