This repository has been archived on 2026-09-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
UserBaseFront/src/views/app/AppForm.vue
T

336 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<Modal v-model="show" :mask-closable="false" width="800" scrollable :title="title"
@on-visible-change="onVisibleChange">
<Form ref="formRef" :model="form" :rules="rules" :label-width="130" class="app-form width-95">
<!-- 第一行:应用名称 / 应用类型 -->
<Row>
<Col span="12">
<FormItem label="应用名称:" prop="name">
<Input v-model="form.name" placeholder="请输入应用名称" />
</FormItem>
</Col>
<Col span="12">
<FormItem label="应用类型:" prop="categoryId">
<Select v-model="form.categoryId" transfer placeholder="请选择应用类型">
<Option v-for="item in categoryList" :key="item.id" :value="item.id" :label="item.name" />
</Select>
</FormItem>
</Col>
</Row>
<!-- 第二行:应用描述(整行) -->
<Row>
<Col span="24">
<FormItem label="应用描述:" prop="desc">
<Input v-model="form.desc" type="text" placeholder="请输入应用描述" />
</FormItem>
</Col>
</Row>
<!-- 第三行:appKey / appSecret(编辑模式才显示) -->
<Row v-if="isEdit">
<Col span="12">
<FormItem label="appKey:">
<Input :model-value="form.appKey?.toString()" readonly />
</FormItem>
</Col>
<Col span="12">
<FormItem label="appSecret:">
<div class="secret-row">
<Input :model-value="form.appSecret" readonly style="flex:1" />
<Button type="default" class="ml-8" @click="$emit('reset-secret')">重置</Button>
</div>
</FormItem>
</Col>
</Row>
<!-- 第四行:应用前端类型(tag 多选) -->
<Row>
<Col span="24">
<FormItem label="应用前端类型:">
<Select v-model="form.clients" multiple transfer placeholder="请选择前端类型" class="client-select">
<Option v-for="opt in clientOptions" :key="opt.value" :value="opt.value" :label="opt.label" />
</Select>
</FormItem>
</Col>
</Row>
<!-- 第五行:Pc应用地址 / Pc应用图标 -->
<Row>
<Col span="12">
<FormItem label="Pc应用地址:">
<Input v-model="form.pcUrl" placeholder="请输入Pc应用地址" />
</FormItem>
</Col>
<Col span="12">
<FormItem label="Pc应用图标:">
<Upload :on-success="(res: any) => onUploadSuccess(res, 'pcIcon')" :show-upload-list="false"
:action="uploadUrl" :data="{ type: 'icon' }" accept=".svg,.png,.jpg,.jpeg"
:with-credentials="true" :headers="{ yschapirequest: 1 }">
<div v-if="!form.pcIcon" class="icon-upload-wrap">
<SvgIcon name="upload-add" color="#646566" style="width: 20px; height: 20px" />
</div>
<div class="icon-preview-wrap" v-else>
<SvgIcon @click.stop="form.pcIcon = null" class="cloase-btn" name="widget-close" size="18"
color="#fff" />
<img :src="previewUrl(form.pcIcon)" alt="" />
</div>
</Upload>
</FormItem>
</Col>
</Row>
<!-- 第六行:App接口地址 / App应用图标 -->
<Row>
<Col span="12">
<FormItem label="App接口地址:">
<Input v-model="form.appUrl" placeholder="请输入App接口地址" />
</FormItem>
</Col>
<Col span="12">
<FormItem label="App应用图标:">
<Upload :on-success="(res: any) => onUploadSuccess(res, 'appIcon')" :show-upload-list="false"
:action="uploadUrl" :data="{ type: 'icon' }" accept=".svg,.png,.jpg,.jpeg"
:with-credentials="true" :headers="{ yschapirequest: 1 }">
<div v-if="!form.appIcon" class="icon-upload-wrap">
<SvgIcon name="upload-add" color="#646566" style="width: 20px; height: 20px" />
</div>
<div class="icon-preview-wrap" v-else>
<SvgIcon @click.stop="form.appIcon = null" class="cloase-btn" name="widget-close" size="18"
color="#fff" />
<img :src="previewUrl(form.appIcon)" alt="" />
</div>
</Upload>
</FormItem>
</Col>
</Row>
<!-- 第七行:公众号应用地址 / 钉钉应用地址 -->
<Row>
<Col span="12">
<FormItem label="公众号应用地址:">
<Input v-model="form.officialAccountUrl" placeholder="请输入公众号应用地址" />
</FormItem>
</Col>
<Col span="12">
<FormItem label="钉钉应用地址:">
<Input v-model="form.dingtalkUrl" placeholder="请输入钉钉应用地址" />
</FormItem>
</Col>
</Row>
</Form>
<template #footer>
<Button type="primary" :loading="loading" @click="handleSubmit">提交</Button>
<Button @click="handleCancel">取消</Button>
</template>
</Modal>
</template>
<script setup lang="ts">
import SvgIcon from "@/components/SvgIcon.vue";
import { computed, reactive, watch } from "vue";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
const { $bus, $utils, $conf } = useCurrentInstance();
/* ---------- Props / Emits ---------- */
const props = withDefaults(defineProps<{
modelValue: boolean;
initForm?: Record<string, any>;
categoryList?: any[];
isEdit?: boolean;
loading?: boolean;
}>(), {
initForm: () => ({}),
categoryList: () => [],
isEdit: false,
loading: false,
});
const emit = defineEmits(['update:modelValue', 'submit', 'cancel', 'reset-secret']);
const show = computed({
get: () => props.modelValue,
set: val => emit('update:modelValue', val),
});
const title = computed(() => props.isEdit ? '编辑应用' : '新增应用');
/* ---------- 表单数据 ---------- */
const defaultForm = (): Record<string, any> => ({
id: null,
name: '',
categoryId: undefined as number | undefined,
desc: '',
appKey: null,
appSecret: '',
clients: [] as number[],
pcUrl: '',
pcIcon: '',
appUrl: '',
appIcon: '',
officialAccountUrl: '',
dingtalkUrl: '',
});
const form = reactive(defaultForm());
// 外部 initForm 变化时回填
watch(() => props.initForm, (val) => {
if (val && Object.keys(val).length > 0) {
Object.assign(form, defaultForm(), val);
} else {
Object.assign(form, defaultForm());
}
}, { immediate: true, deep: true });
/* ---------- 上传相关 ---------- */
// 上传地址使用 $conf.serviceHost 作为基地址
const uploadUrl = computed(() => $conf?.serviceHost + '/api/base/UploadFile');
function previewUrl(path: string | undefined): string {
if (!path) return '';
if (path.startsWith('http')) return path;
return ($conf?.serviceHost || '') + path;
}
function onUploadSuccess(response: any, field: 'pcIcon' | 'appIcon') {
if (response?.code === 0 || response?.code === 200) {
(form as any)[field] = response.data ?? response.url ?? '';
} else {
Message.error((response?.msg || response?.message) || '上传失败');
}
}
/* ---------- 前端类型选项 ---------- */
const clientOptions = [
{ value: 1, label: 'Pc浏览器' },
{ value: 2, label: '移动App' },
];
/* ---------- 校验规则 ---------- */
const rules = {
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
categoryId: [{ required: true, type: 'number' as const, message: '请选择应用类型', trigger: 'change' }],
};
/* ---------- 提交 ---------- */
const formRef = ref();
async function handleSubmit() {
try {
await formRef.value?.validate();
} catch {
return false;
}
// 提交前清理空值
const submitData = { ...form };
emit('submit', submitData);
}
function handleCancel() {
emit('cancel');
}
function onVisibleChange(val: boolean) {
if (!val) {
emit('cancel');
}
}
</script>
<style lang="scss" scoped>
/* 表单整体:宽度 95% 避免横向滚动(width-95 工具类控制宽度,scss 生成) */
.app-form {
margin: 0 auto;
max-height: 60vh;
overflow-y: auto;
overflow-x: hidden;
padding-right: 12px;
/* FormItem 改为 block,避免默认 inline-block 在 flex Col 内垂直间距失效 */
:deep(.ivu-form-item) {
display: block;
margin-bottom: 0;
}
:deep(.ivu-form-item-label) {
font-size: 13px;
color: #515a6e;
}
/* 行间距改由 Row 统一控制(Row 是 flex 容器,FormItem 默认无 margin-bottom) */
:deep(.ivu-row) {
margin-left: 0 !important;
margin-right: 0 !important;
margin-bottom: 22px;
}
:deep(.ivu-row:last-child) {
margin-bottom: 0;
}
}
.secret-row {
display: flex;
align-items: center;
width: 100%;
}
/* 图标上传区域 */
.icon-upload-wrap {
width: 48px;
height: 48px;
border: 1px dashed #dcdee2;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color 0.2s;
&:hover {
border-color: #577afb;
}
}
.icon-preview-wrap {
position: relative;
width: 48px;
height: 48px;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cloase-btn {
position: absolute;
top: 0;
right: 0;
background: rgba(0, 0, 0, 0.45);
border-radius: 0 0 0 6px;
padding: 2px;
cursor: pointer;
&:hover {
background: rgba(220, 50, 50, 0.8);
}
}
}
/* 客户端多选样式 */
.client-select {
width: 100%;
:deep(.ivu-select-selection) {
max-height: 100px;
overflow-y: auto;
}
}
</style>