Archived
forked from admin/userbase
352 lines
12 KiB
Vue
352 lines
12 KiB
Vue
<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" v-if="!schoolMode">
|
||
<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>
|
||
|
||
<!-- 学校模式说明:仅 名称/描述/图标 可编辑,且仅对本校生效 -->
|
||
<Row v-if="schoolMode">
|
||
<Col span="24">
|
||
<Alert show-icon>学校模式:此处修改仅对本校生效(应用别名),平台主数据与密钥/推送等配置不可修改。</Alert>
|
||
</Col>
|
||
</Row>
|
||
|
||
<!-- 第三行:appKey / appSecret(编辑模式且非学校模式才显示) -->
|
||
<Row v-if="isEdit && !schoolMode">
|
||
<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 v-if="!schoolMode">
|
||
<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应用图标(学校模式仅保留 Pc应用图标) -->
|
||
<Row>
|
||
<Col span="12" v-if="!schoolMode">
|
||
<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 v-if="!schoolMode">
|
||
<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 v-if="!schoolMode">
|
||
<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;
|
||
/** 学校管理员模式:仅 名称/描述/Pc图标 三字段可编辑(写学校覆盖表) */
|
||
schoolMode?: boolean;
|
||
}>(), {
|
||
initForm: () => ({}),
|
||
categoryList: () => [],
|
||
isEdit: false,
|
||
loading: false,
|
||
schoolMode: 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) || '上传失败');
|
||
}
|
||
}
|
||
|
||
/* ---------- 前端类型选项 ---------- */
|
||
// value 对齐后端平台枚举 com.yunhai.platform.utils.enums.ClientEnum(位掩码 key):
|
||
// 1=Pc浏览器 2=移动App 8=微信公众号 32=钉钉;公众号/钉钉地址见表单第七行
|
||
const clientOptions = [
|
||
{ value: 1, label: 'Pc浏览器' },
|
||
{ value: 2, label: '移动App' },
|
||
{ value: 8, label: '微信公众号' },
|
||
{ value: 32, label: '钉钉' },
|
||
];
|
||
|
||
/* ---------- 校验规则(学校模式不校验 categoryId——类型字段不展示且不可改) ---------- */
|
||
const rules = computed(() => ({
|
||
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
|
||
...(props.schoolMode ? {} : {
|
||
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>
|