0.0.3
springboot版本升级到3.4.1 修复已知问题 新增微信第三方平台 新增万能表单
7
.gitignore
vendored
@ -1,9 +1,11 @@
|
||||
HELP.md
|
||||
target/
|
||||
logs/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
@ -31,3 +33,8 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
admin/auto-imports.d.ts
|
||||
admin/components.d.ts
|
||||
logs/**
|
||||
webroot/runtime/upgrade
|
||||
1
admin/auto-imports.d.ts
vendored
@ -1,6 +1,5 @@
|
||||
// Generated by 'unplugin-auto-import'
|
||||
export {}
|
||||
declare global {
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
const ElNotification: typeof import('element-plus/es')['ElNotification']
|
||||
}
|
||||
|
||||
14
admin/components.d.ts
vendored
@ -11,15 +11,8 @@ declare module '@vue/runtime-core' {
|
||||
DiyLink: typeof import('./src/components/diy-link/index.vue')['default']
|
||||
Editor: typeof import('./src/components/editor/index.vue')['default']
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCarousel: typeof import('element-plus/es')['ElCarousel']
|
||||
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
@ -47,10 +40,7 @@ declare module '@vue/runtime-core' {
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElOptionGroup: typeof import('element-plus/es')['ElOptionGroup']
|
||||
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
@ -58,7 +48,6 @@ declare module '@vue/runtime-core' {
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRate: typeof import('element-plus/es')['ElRate']
|
||||
ElResult: typeof import('element-plus/es')['ElResult']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
@ -67,7 +56,6 @@ declare module '@vue/runtime-core' {
|
||||
ElStatistic: typeof import('element-plus/es')['ElStatistic']
|
||||
ElStep: typeof import('element-plus/es')['ElStep']
|
||||
ElSteps: typeof import('element-plus/es')['ElSteps']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
@ -76,8 +64,8 @@ declare module '@vue/runtime-core' {
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
ExportSure: typeof import('./src/components/export-sure/index.vue')['default']
|
||||
|
||||
@ -60,7 +60,7 @@ export function editDiyPage(params: Record<string, any>) {
|
||||
* @param params
|
||||
*/
|
||||
export function setUseDiyPage(params: Record<string, any>) {
|
||||
return request.put(`diy/use`, params, {showSuccessMessage: true})
|
||||
return request.put(`diy/use/${params.id}`, params, {showSuccessMessage: true})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -189,4 +189,64 @@ export function changeTemplate(params: Record<string, any>) {
|
||||
*/
|
||||
export function getApps(params: Record<string, any>) {
|
||||
return request.get(`diy/apps`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制模版页面
|
||||
* @param params
|
||||
*/
|
||||
export function copyDiy(params: Record<string, any>) {
|
||||
return request.post(`diy/copy`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/***************************************************** 主题风格 ****************************************************/
|
||||
|
||||
|
||||
/**
|
||||
* 获取默认主题配色
|
||||
* @param params
|
||||
*/
|
||||
export function getDefaultTheme(params: Record<string, any>) {
|
||||
return request.get(`diy/theme/color`, {params})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取自定义主题配色
|
||||
* @param params
|
||||
*/
|
||||
export function getDiyTheme(params: Record<string, any>) {
|
||||
return request.get(`diy/theme`, {params})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加配色
|
||||
* @param params
|
||||
*/
|
||||
export function addTheme(params: Record<string, any>) {
|
||||
return request.post(`diy/theme/add`, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑配色
|
||||
* @param params
|
||||
*/
|
||||
export function editTheme(params: Record<string, any>) {
|
||||
return request.put(`diy/theme/edit/${params.id}`, params, {showSuccessMessage: true})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配色
|
||||
* @param params
|
||||
*/
|
||||
export function deleteTheme(id: number) {
|
||||
return request.delete(`diy/theme/delete/${id}`, {showSuccessMessage: true})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主题配色
|
||||
* @param params
|
||||
*/
|
||||
export function setDiyTheme(params: Record<string, any>) {
|
||||
return request.post(`diy/theme`, params, { showSuccessMessage: true })
|
||||
}
|
||||
236
admin/src/app/api/diy_form.ts
Normal file
@ -0,0 +1,236 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/***************************************************** 万能表单 ****************************************************/
|
||||
|
||||
/**
|
||||
* 获取万能表单分页列表
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getDiyFormPageList(params: Record<string, any>) {
|
||||
return request.get(`diy/form`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取万能表单列表
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getDiyFormList(params: Record<string, any>) {
|
||||
return request.get(`diy/form/list`, { params })
|
||||
}
|
||||
/**
|
||||
* 获取万能表单分页列表(用于弹框选择)
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getDiyFormSelectPageList(params: Record<string, any>) {
|
||||
return request.get(`diy/form/select`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取万能表单详情
|
||||
* @param form_id 万能表单id
|
||||
* @returns
|
||||
*/
|
||||
export function getDiyFormInfo(form_id: number) {
|
||||
return request.get(`diy/form/${ form_id }`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加万能表单
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function addDiyForm(params: Record<string, any>) {
|
||||
return request.post('diy/form', params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑万能表单
|
||||
* @param params
|
||||
*/
|
||||
export function editDiyForm(params: Record<string, any>) {
|
||||
return request.put(`diy/form/${ params.form_id }`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改万能表单分享内容
|
||||
* @param params
|
||||
*/
|
||||
export function editDiyFormShare(params: Record<string, any>) {
|
||||
return request.put(`diy/form/share`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除万能表单
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function deleteDiyForm(params: Record<string, any>) {
|
||||
return request.put(`diy/form/delete`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取万能表单初始化数据
|
||||
*/
|
||||
export function initPage(params: Record<string, any>) {
|
||||
return request.get(`diy/form/init`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取万能表单微信小程序二维码
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getDiyFormQrcode(params: Record<string, any>) {
|
||||
return request.get(`diy/form/qrcode`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取万能表单字段列表
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getDiyFormFieldsList(params: Record<string, any>) {
|
||||
return request.get(`diy/form/fields/list`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段统计列表
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getDiyFormFieldStat(params: Record<string, any>) {
|
||||
return request.get(`diy/form/records/field/stat`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取页面模板类型
|
||||
*/
|
||||
export function getDiyTemplate(params: Record<string, any>) {
|
||||
return request.get(`diy/template`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板页面列表
|
||||
*/
|
||||
export function getDiyTemplatePages(params: Record<string, any>) {
|
||||
return request.get(`diy/form/template`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 万能表单状态状态
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function editFormStatus(params: Record<string, any>) {
|
||||
return request.put(`diy/form/status`, params, {
|
||||
showErrorMessage: true,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板页面(存在的应用插件列表)
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getApps(params: Record<string, any>) {
|
||||
return request.get(`diy/apps`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制模版页面
|
||||
* @param params
|
||||
*/
|
||||
export function copyDiy(params: Record<string, any>) {
|
||||
return request.post(`diy/form/copy`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取万能表单类型
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getFormType(params: Record<string, any>) {
|
||||
return request.get(`diy/form/type`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取万能表单填写配置
|
||||
* @param form_id
|
||||
* @returns
|
||||
*/
|
||||
export function getFormWriteConfig(form_id: any) {
|
||||
return request.get(`diy/form/write/${ form_id }`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑万能表单填写配置
|
||||
* @param params
|
||||
*/
|
||||
export function editDiyFormWriteConfig(params: Record<string, any>) {
|
||||
return request.put(`diy/form/write`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取万能表单提交成功页配置
|
||||
* @param form_id
|
||||
* @returns
|
||||
*/
|
||||
export function getFormSubmitConfig(form_id: any) {
|
||||
return request.get(`diy/form/submit/${ form_id }`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑万能表单提交成功页配置
|
||||
* @param params
|
||||
*/
|
||||
export function editDiyFormSubmitConfig(params: Record<string, any>) {
|
||||
return request.put(`diy/form/submit`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取万能表单数据列表
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getFormRecords(params: Record<string, any>) {
|
||||
return request.get(`diy/form/records`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取万能表单数据详情
|
||||
* @param id
|
||||
* @returns
|
||||
*/
|
||||
export function getFormRecordsInfo(id: number) {
|
||||
return request.get(`diy/form/records/${ id }`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除万能表单数据
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function deleteFormRecords(params: Record<string, any>) {
|
||||
return request.put(`diy/form/records/delete`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取万能表单填表人列表
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getFormRecordsMember(params: Record<string, any>) {
|
||||
return request.get(`diy/form/records/member/stat`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制模版页面
|
||||
* @param params
|
||||
*/
|
||||
export function copyForm(params: Record<string, any>) {
|
||||
return request.post(`diy/form/copy`, params, { showSuccessMessage: true })
|
||||
}
|
||||
@ -249,3 +249,11 @@ export function getAccountType() {
|
||||
export function getSiteAddons() {
|
||||
return request.get('site/addons')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取站点应用
|
||||
* @returns
|
||||
*/
|
||||
export function getShowApp() {
|
||||
return request.get('site/showApp')
|
||||
}
|
||||
|
||||
@ -61,8 +61,8 @@ export function getWeappVersionList(params: Record<string, any>) {
|
||||
|
||||
/**
|
||||
* 获取微信小程序上传日志
|
||||
* @param key
|
||||
* @returns
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
export function getWeappUploadLog(key: string) {
|
||||
return request.get(`weapp/upload/${key}`)
|
||||
@ -130,4 +130,28 @@ export function deleteVersion(id: string) {
|
||||
*/
|
||||
export function getIsTradeManaged() {
|
||||
return request.get('weapp/delivery/getIsTradeManaged')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务器域名
|
||||
* @param params
|
||||
*/
|
||||
export function setWeappDomain(params: Record<string, any>) {
|
||||
return request.put('weapp/domain', params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置小程序隐私协议
|
||||
* @param params
|
||||
*/
|
||||
export function setWeappPrivacySetting(params: Record<string, any>) {
|
||||
return request.put('weapp/privacysetting', params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取小程序隐私协议
|
||||
* @param params
|
||||
*/
|
||||
export function getWeappPrivacySetting() {
|
||||
return request.get('weapp/privacysetting')
|
||||
}
|
||||
|
||||
BIN
admin/src/app/assets/images/diy_form/mobile_bottom.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
admin/src/app/assets/images/diy_form/mobile_line.png
Normal file
|
After Width: | Height: | Size: 167 B |
BIN
admin/src/app/assets/images/diy_form/mobile_tabbar.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 30 KiB |
BIN
admin/src/app/assets/images/index/app_store.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
admin/src/app/assets/images/index/install.png
Normal file
|
After Width: | Height: | Size: 838 B |
BIN
admin/src/app/assets/images/index/not_install.png
Normal file
|
After Width: | Height: | Size: 964 B |
BIN
admin/src/app/assets/images/index/site2.png
Normal file
|
After Width: | Height: | Size: 880 B |
BIN
admin/src/app/assets/images/index/site3.png
Normal file
|
After Width: | Height: | Size: 1011 B |
BIN
admin/src/app/assets/images/index/site_add.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
admin/src/app/assets/images/index/site_list.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
admin/src/app/assets/images/index/site_normal.png
Normal file
|
After Width: | Height: | Size: 957 B |
BIN
admin/src/app/assets/images/index/site_tc.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
admin/src/app/assets/images/index/site_user.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
admin/src/app/assets/images/login/login_bg.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
admin/src/app/assets/images/login/login_icon.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
admin/src/app/assets/images/login/login_index_bg.jpg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
admin/src/app/assets/images/login/password.png
Normal file
|
After Width: | Height: | Size: 466 B |
BIN
admin/src/app/assets/images/login/site_login_bg.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
admin/src/app/assets/images/login/username.png
Normal file
|
After Width: | Height: | Size: 602 B |
BIN
admin/src/app/assets/images/logo.default.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 24 KiB |
BIN
admin/src/app/assets/images/site_default.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 33 KiB |
@ -42,5 +42,6 @@
|
||||
"uploadSuccessTips": "小程序上传成功后还需到<a href='https://mp.weixin.qq.com/' target='_blank' class='text-primary'>微信公众平台</a>提交审核,审核通过后发布才算正式上线。",
|
||||
"knownToKnow": "我已知晓,不需要再次提示",
|
||||
"siteAuthTips": "上传代码需先绑定授权码,请联系平台管理员进行绑定",
|
||||
"againUpload": "重新上传"
|
||||
"againUpload": "重新上传",
|
||||
"uploadWeapp": "上传小程序"
|
||||
}
|
||||
|
||||
@ -34,5 +34,26 @@
|
||||
"weappUpload": "小程序代码上传",
|
||||
"uploadKey": "上传密钥",
|
||||
"uploadKeyTips": "配置之后可实现在线上传小程序版本",
|
||||
"uploadIpTips": "如果小程序代码上传开启了ip白名单设置,在ip白名单中添加ip:"
|
||||
}
|
||||
"uploadIpTips": "如果小程序代码上传开启了ip白名单设置,在ip白名单中添加ip:",
|
||||
"update": "修改",
|
||||
"udpUrl": "udp合法域名",
|
||||
"tcpUrl": "tcp合法域名",
|
||||
"requestdomainPlaceholder": "以 https:// 开头。域名间请用 ; 分割",
|
||||
"wsrequestdomainPlaceholder": "以 wss:// 开头。域名间请用 ; 分割",
|
||||
"uploaddomainPlaceholder": "以 https:// 开头。域名间请用 ; 分割",
|
||||
"downloaddomainPlaceholder": "以 https:// 开头。域名间请用 ; 分割",
|
||||
"udpdomainPlaceholder": "以 udp:// 开头。域名间请用 ; 分割",
|
||||
"tcpdomainPlaceholder": "以 tcp:// 开头。域名间请用 ; 分割",
|
||||
"domainError": "该域名协议头非法",
|
||||
"serviceContentStatement": "服务内容声明",
|
||||
"privacyAgreement": "用户隐私保护指引",
|
||||
"privacyAgreementTips": "基于微信提供的 标准化用户隐私保护指引,根据小程序实际情况更新并展示给用户。",
|
||||
"setting": "设置",
|
||||
"privacyAgreementTitle": "用户隐私保护指引设置",
|
||||
"settingPlaceholder": "请填写用途",
|
||||
"addSettingType": "增加信息类型",
|
||||
"settingTypeTitle": "使用用户信息类型",
|
||||
"addContact": "增加联系方式",
|
||||
"addSdkInfo": "增加第三方SDK信息",
|
||||
"addSdkSettingList": "增加SDK提供方的隐私信息"
|
||||
}
|
||||
|
||||
@ -118,6 +118,7 @@
|
||||
"textAlign": "对齐方式",
|
||||
"textAlignLeft": "居左",
|
||||
"textAlignCenter": "居中",
|
||||
"textAlignRight": "居右",
|
||||
"textSet": "文字设置",
|
||||
"textFontSize": "文字大小",
|
||||
"textFontWeight": "文字粗细",
|
||||
@ -257,8 +258,9 @@
|
||||
"horzLineStyleDashed": "虚线",
|
||||
"horzLineBorderColor": "线条颜色",
|
||||
"horzLineBorderWidth": "线条宽度",
|
||||
"floatBtnBtton": "按钮位置",
|
||||
"floatBtnButton": "按钮位置",
|
||||
"floatBtnOffset": "上下偏移",
|
||||
"lateralBtnOffset": "左右偏移",
|
||||
"floatBtnImageSet": "图片设置",
|
||||
"floatBtnImageSize": "图片大小",
|
||||
"floatBtnAroundRadius": "图片圆角",
|
||||
@ -283,9 +285,111 @@
|
||||
"pictureShowBtnBgColor": "背景颜色",
|
||||
"pictureShowBlockStyle": "模块样式",
|
||||
|
||||
"fieldNamePlaceholder": "请输入字段名称",
|
||||
"fieldRemarkPlaceholder": "请输入字段说明",
|
||||
"defaultValue": "默认值",
|
||||
"defaultValuePlaceholder": "请输入默认值",
|
||||
"formPlaceholder": "提示语",
|
||||
"formPlaceholderTips": "请输入提示语",
|
||||
"isRequired": "是否必填"
|
||||
"isRequired": "是否必填",
|
||||
"optionPlaceholder": "请输入选项内容",
|
||||
"formLayout": "表单布局",
|
||||
"layoutStyle": "排版风格",
|
||||
"singleTiling": "单列平铺",
|
||||
"singleTilingTipsOne": "将所有需要填写的表单内容项直接罗列在页面上。",
|
||||
"singleTilingTipsTwo": "适用于表单内容项较少且项目之间无逻辑关系的情况。",
|
||||
"singleTilingTipsThree": "其优势在于相对简洁、便于操作",
|
||||
"singleTilingTipsFour": "但当表单项数量较大时,一次性展示全部信息会增加用户的操作负担,填写效率较低",
|
||||
"arrangeSideBySide": "左右排列",
|
||||
"arrangeSideBySideTipsOne": "将表单分为左右两部分,左侧为标题和描述,右侧为输入区域。",
|
||||
"arrangeSideBySideTipsTwo": "这种布局适用于标题和描述内容较少的情况,能够提高表单的紧凑性和用户体验。",
|
||||
"layoutStyleTips": "切换后将同步所有表单组件的展示形式",
|
||||
"borderControl": "边框开关",
|
||||
"fieleContent": "字段内容",
|
||||
"fieldName": "字段名称",
|
||||
"filedRemark": "字段说明",
|
||||
"otherSetting": "其他设置",
|
||||
"hideControl": "隐藏该组件",
|
||||
"hideControlTipsOne": "勾选后填表人填表时看不到该字段。",
|
||||
"hideControlTipsTwo": "适用于你不再收集该字段又不希望删除已收集的数据。",
|
||||
"textStyle": "文字样式",
|
||||
"filedRemarkStyle": "字段说明样式",
|
||||
|
||||
"style": "样式",
|
||||
"listStyle": "列表",
|
||||
"dropDownStyle": "下拉",
|
||||
"option": "选项",
|
||||
"addSingleOption": "添加单个选项",
|
||||
"addMultipleOption": "批量添加选项",
|
||||
"addOptionTips": "每个选项之间用英文“,” 隔开,自动过滤重复内容",
|
||||
"errorTipsOne": "存在重复选项,请检查内容",
|
||||
"errorTipsTwo": "选项已存在,请重新输入",
|
||||
|
||||
"dataFormat": "日期格式",
|
||||
"startDate": "开始日期",
|
||||
"startTime": "开始时间",
|
||||
"startDataTips": "开始日期不能为空",
|
||||
"startTimeTips": "开始时间不能为空",
|
||||
"startDataPlaceholder": "请选择开始日期",
|
||||
"dataPlaceholder": "请选择日期",
|
||||
"timePlaceholder": "请选择时间",
|
||||
"startTimePlaceholder": "请选择开始时间",
|
||||
"endDate": "结束日期",
|
||||
"endTime": "结束时间",
|
||||
"endDataPlaceholder": "请选择结束日期",
|
||||
"endTimePlaceholder": "请选择结束时间",
|
||||
"endDataTips": "结束日期不能为空",
|
||||
"endTimeTips": "结束时间不能为空",
|
||||
"startEndDataTips": "开始日期不能大于结束日期",
|
||||
"startEndTimeTips": "开始时间不能大于结束时间",
|
||||
"currentDate": "当天日期",
|
||||
"diyDate": "指定日期",
|
||||
"currentTime": "当天时间",
|
||||
"diyTime": "指定时间",
|
||||
|
||||
"preventDuplication":"内容防重复",
|
||||
"preventDuplicationTipsOne":"该组件填写的内容不能与已提交的数据重复。",
|
||||
"preventDuplicationTipsTwo":"极端情况下可能存在延时导致限制失效。",
|
||||
"privacyProtection":"隐私保护",
|
||||
"privacyProtectionTipsOne":"会自动将提交的个人信息做加密展示。",
|
||||
"privacyProtectionTipsTwo":"适用于公开展示收集的数据且不暴露用户隐私。",
|
||||
"privacyProtectionTipsThree":"提交后自动隐藏中间11位数字,仅管理员可查看",
|
||||
"privacyProtectionTipsFour":"提交后自动隐藏文本,仅管理员可查看",
|
||||
"privacyProtectionTipsFive":"提交后自动隐藏中间5位数字,仅管理员可查看",
|
||||
|
||||
"imageLimit":"限制数量",
|
||||
"imageLimitPlaceholder":"请输入限制数量",
|
||||
"imageLimitErrorTips":"限制数量格式输入错误",
|
||||
"imageLimitErrorTipsTwo":"限制数量不能小于0",
|
||||
"imageLimitErrorTipsThree":"限制数量必须大于0",
|
||||
"imafeLimitErrorTipsFour":"限制数量最大不能超过9",
|
||||
|
||||
"defaultValueTips":"设置后,默认值会自动填充到输入框,填表人可在此基础上进行修改。",
|
||||
"defaultErrorTips":"默认值格式输入错误",
|
||||
"defaultMustZeroTips":"默认值不能小于0",
|
||||
|
||||
"access":"获取方式",
|
||||
"authorizeWeChatLocation":"授权微信定位",
|
||||
"manuallySelectPositioning":"手动选择定位",
|
||||
|
||||
"unit":"单位",
|
||||
"unitPlaceholder":"请输入单位",
|
||||
|
||||
"followContent":"跟随内容",
|
||||
"hoverScreenBottom":"悬浮屏幕底部",
|
||||
"btnTips":"当表单内容多时,只有滚动页面至最底部才会显示提交按钮",
|
||||
"btnTipsTwo":"当表单内容多时,滚动页面至最底部时,提交按钮会自动按钮悬浮在屏幕底部,方便填表人快速提交显示在屏幕底部",
|
||||
"btnTipsThree":"若前端以嵌入形式调用表单,提交按钮组件将不显示,相关业务由该页面负责处理",
|
||||
"submitBtn":"提交按钮",
|
||||
"submitBtnName":"按钮名称",
|
||||
"btnNamePlaceholder":"请输入按钮名称",
|
||||
"submitBtnNamePlaceholder":"请输入提交按钮名称",
|
||||
"resetBtn":"重置按钮",
|
||||
"btnStyle":"按钮样式",
|
||||
"resetBtnNamePlaceholder":"请输入重置按钮名称",
|
||||
|
||||
"rowCount":"显示行数",
|
||||
"rowCountPlaceholder":"请输入显示行数"
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -18,8 +18,8 @@
|
||||
"copySuccess": "复制成功",
|
||||
"titlePlaceholder": "请输入表单名称",
|
||||
"addDiyForm": "添加表单",
|
||||
"diyFormDeleteTips": "确定要删除该自定义表单吗?",
|
||||
"diyFormCopyTips": "确定要复制该自定义表单吗?",
|
||||
"diyFormDeleteTips": "确定要删除该表单吗?",
|
||||
"diyFormCopyTips": "确定要复制该表单吗?",
|
||||
"preview": "预览",
|
||||
"share": "分享",
|
||||
"shareSet": "分享设置",
|
||||
@ -30,5 +30,99 @@
|
||||
"shareTitlePlaceholder": "请输入分享标题",
|
||||
"shareDesc": "分享描述",
|
||||
"shareDescPlaceholder": "请输入分享描述",
|
||||
"shareImageUrl": "分享图片"
|
||||
"shareImageUrl": "分享图片",
|
||||
|
||||
"joinMemberType": "参与会员",
|
||||
"allMember": "所有会员参与",
|
||||
"selectedMemberLevel": "指定会员等级",
|
||||
"selectedMemberLabel": "指定会员标签",
|
||||
"memberLevel": "会员等级",
|
||||
"memberLevelPlaceholder": "请选择会员等级",
|
||||
"memberLabel": "会员标签",
|
||||
"memberLabelPlaceholder": "请选择会员标签",
|
||||
"labelTips": "请选择会员标签",
|
||||
"levelTips": "请选择会员等级",
|
||||
|
||||
"batchDeletion": "批量删除",
|
||||
"batchEmptySelectedFormsTips": "请选择要删除的表单",
|
||||
"batchFormsDeleteTips": "确定要删除选中的表单吗?",
|
||||
"promotion":"推广",
|
||||
"submitSuccess":"提交成功页",
|
||||
"writeSet":"填写设置",
|
||||
"export":"导出",
|
||||
"detail":"详情",
|
||||
"more":"更多",
|
||||
|
||||
"formPromotion":"表单推广",
|
||||
"promoteUrl":"推广链接",
|
||||
"downLoadQRCode":"下载二维码",
|
||||
"configureFailed":"配置失败",
|
||||
|
||||
"writeSuccess":"填写成功",
|
||||
"viewFillingDetails":"查看填写详情",
|
||||
"finish":"完成",
|
||||
"finishTips":"点击后进入首页",
|
||||
"back":"返回",
|
||||
"backTips":"点击后返回表单",
|
||||
"afterSubmission":"填表人提交后",
|
||||
"displayTextMessages":"显示文字消息",
|
||||
"displayTextMessagesTips":"提交后页面显示文字信息。",
|
||||
"promptText":"提示文字",
|
||||
"defaultPrompt":"默认提示",
|
||||
"defaultPromptTips":"将显示: 填写成功",
|
||||
"diyPrompt":"自定义提示",
|
||||
"tipsTextPlaceholder":"感谢你的填写",
|
||||
"subsequentPperationButtons":"后续操作按钮",
|
||||
|
||||
"validityPeriodOfVoucher":"凭证有效期",
|
||||
"noLimit":"不限制",
|
||||
"specifyTime":"设置固定有效期",
|
||||
"specifyTimeTips":"每条记录的凭证有效期都是一样的,例如:会议凭证可设置有效期为会议举行时间。",
|
||||
"submissionTime":"按提交时间设置有效期",
|
||||
"submissionTimeTips":"每条记录的凭证有效期按照提交时间来计算,例如:优惠凭证的有效期可以设置为领取后3天内有效。",
|
||||
"afterSubmissionRecords":"提交记录后",
|
||||
"effective":"有效",
|
||||
|
||||
"voucherStyle":"凭证样式",
|
||||
"titleAboveTheCode":"码上方标题",
|
||||
"titleAboveTheCodePlaceholder":"请妥善保存你的核销凭证",
|
||||
"contentAboveTheCode":"码上方内容",
|
||||
"contentAboveTheCodePlaceholder":"请输入码上方内容",
|
||||
"addLinefeeds":"添加换行符",
|
||||
"addFields":"添加字段",
|
||||
"contentBelowTheCode":"码下方内容",
|
||||
"submissionRecordTime":"展示提交记录时间",
|
||||
"currentTime":"展示当前时间",
|
||||
"currentTimeTips":"会以秒进行跳动,可起到防作假的功能",
|
||||
"voucherDeadline":"展示凭证截止时间",
|
||||
"saveVoucher":"支持填表人保存凭证",
|
||||
"dispalyPromptText":"展示提示文字",
|
||||
|
||||
"diy":"自定义",
|
||||
"apieceFillQuantity":"每人可填写次数",
|
||||
"fillQuantityTotal":"表单可填写总数",
|
||||
"writeTips":"填写限制的校验在极端情况下可能存在延时,从而导致限制失效,不建议商品限时抢购等场景使用该功能",
|
||||
"fillInTheTimePeriod":"可填写时间段",
|
||||
"setSpecifyTime":"设置开始/停止时间",
|
||||
"openDayTime":"设置每日开启时间",
|
||||
"timeLimitRuleOne":"开始/停止时间不能为空",
|
||||
"timeLimitRuleTwo":"开启时间不能为空",
|
||||
"timeLimitRuleThree":"开始时间不能等于结束时间",
|
||||
"numCannotNull":"次数不能为空",
|
||||
|
||||
"dataAndStatistics":"数据与统计",
|
||||
"detailData":"明细数据",
|
||||
"fillInFormPerson":"填表人",
|
||||
"fillInFormPersonplaceholder":"请输入填表人",
|
||||
"fillInFormDate":"填表时间",
|
||||
"fillInFormPersonInfo":"填表人信息",
|
||||
"fillInFormPersonStatics":"填表人统计",
|
||||
"fillInFormTotal":"总计(表单填写数)",
|
||||
"fieldStatistics":"字段统计",
|
||||
"viewInformation":"查看信息",
|
||||
"startDate":"开始时间",
|
||||
"endDate":"结束时间",
|
||||
|
||||
"deleteTips":"确定删除该条数据吗"
|
||||
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"signPeriod": "签到周期",
|
||||
"signPeriodTip": "请输入签到周期",
|
||||
"signPeriodLimitTips": "签到周期格式错误",
|
||||
"signPeriodMustZeroTips": "签到周期必须大于0",
|
||||
"signPeriodMustZeroTips": "签到周期为2-365天",
|
||||
"calendarSign": "日历签到",
|
||||
"periodSign": "周期签到",
|
||||
"daySignAward": "日签奖励",
|
||||
@ -26,12 +26,16 @@
|
||||
"ruleExplainTip": "请输入规则说明",
|
||||
"ruleExplainDefault": "1.每日签到可以获得日签奖励,连续签到可以获得连签奖励;\n2.每日最多可签到1次,断签则会重新计算连签天数;\n3.活动以及奖励最终解释权归商家所有。",
|
||||
"useDefaultExplain": "使用默认说明",
|
||||
"continueSign": "连续签到天数",
|
||||
"continueSign": "连签天数",
|
||||
"continueSignFormatError": "连签天数格式错误",
|
||||
"continueSignBerweenDays": "连签天数为2-365天",
|
||||
"receiveLimit": "领取限制",
|
||||
"noLimit": "不限制",
|
||||
"everyOneLimit": "每人限领",
|
||||
"time": "次",
|
||||
"day": "天",
|
||||
"continueSignPlaceholder":"请输入连续签到天数",
|
||||
"receiveNumPlaceholder":"请输入限领次数"
|
||||
"continueSignPlaceholder":"请输入连签天数",
|
||||
"receiveNumPlaceholder":"请输入限领次数",
|
||||
"receiveNumFormatError":"限领次数格式错误",
|
||||
"receiveNumMustGreaterThanZeroTip":"限领次数不能小于等于0"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"memberId":"会员编号",
|
||||
"memberInfoPlaceholder":"请输入会员信息",
|
||||
"memberInfoPlaceholder":"请输入会员编码/昵称/手机号搜索",
|
||||
"memberInfo":"会员信息",
|
||||
"mobile":"手机号码",
|
||||
"nickName":"会员昵称",
|
||||
|
||||
@ -3,30 +3,40 @@
|
||||
<div class="main-container" v-loading="loading">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">应用管理</span>
|
||||
</div>
|
||||
<template v-if="Object.keys(appList).length">
|
||||
|
||||
<div class="flex flex-wrap plug-list pb-10 plug-large" v-if="appList.length">
|
||||
<div v-for="(item, index) in appList" :key="index + 'b'" class="app-item relative cursor-pointer mt-[20px] mr-4 bg-[#f7f7f7] w-[264px] flex py-[20px] px-[17px]" @click="toLink(item.key)">
|
||||
<el-image class="w-[40px] h-[40px] mr-[10px]" :src="img(item.icon)" fit="contain">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<img class="w-[50px] h-[50px]" src="@/app/assets/images/index/app_default.png" />
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="flex flex-col justify-between w-[180px]">
|
||||
<div class="text-[14px] flex items-center">
|
||||
<span class="app-text max-w-[170px]">{{ item.title }}</span>
|
||||
<span class="iconfont iconxiaochengxu2 text-[#00b240] ml-[4px] !text-[14px]"></span>
|
||||
</div>
|
||||
<p class="app-text text-[12px] text-[#999]">{{item.desc}}</p>
|
||||
<template v-for="(item, index) in appList" :key="index + 'b'">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ item.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty flex items-center justify-center" v-if="!loading && !appList.length">
|
||||
<div class="flex flex-wrap plug-list pb-10 plug-large">
|
||||
<div class="cursor-pointer mt-[20px] mr-4 bg-[#f7f7f7]" v-for="(childItem,childIndex) in item.list" :key="childIndex" @click="toLink(childItem)">
|
||||
<div class="w-[264px] flex py-[20px] px-[17px] app-item relative">
|
||||
<el-image class="w-[40px] h-[40px] mr-[10px]" :src="img(childItem.icon)" fit="contain">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<img class="w-[40px] h-[40px]" src="@/app/assets/images/index/app_default.png" />
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="flex flex-col justify-between w-[180px]">
|
||||
<div class="text-[14px] flex items-center">
|
||||
<span class="app-text max-w-[170px]">{{ childItem.title }}</span>
|
||||
<span class="iconfont iconxiaochengxu2 text-[#00b240] ml-[4px] !text-[14px]"></span>
|
||||
</div>
|
||||
<!-- <el-icon color="#666">
|
||||
<QuestionFilled />
|
||||
</el-icon> -->
|
||||
<p class="app-text text-[12px] text-[#999]">{{childItem.desc}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div class="empty flex items-center justify-center" v-if="!loading && !Object.keys(appList).length">
|
||||
<el-empty :description="t('emptyAppData')" />
|
||||
</div>
|
||||
</el-card>
|
||||
@ -35,7 +45,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { getSiteAddons } from '@/app/api/site'
|
||||
import { getSiteAddons,getShowApp } from '@/app/api/site'
|
||||
import { img } from '@/utils/common'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
@ -47,14 +57,23 @@ const appList = ref<Record<string, any>[]>([])
|
||||
|
||||
const loading = ref(true)
|
||||
const getAppList = async () => {
|
||||
const res = await getSiteAddons()
|
||||
// const res = await getSiteAddons()
|
||||
// appList.value = res.data
|
||||
// loading.value = false
|
||||
|
||||
const res = await getShowApp();
|
||||
appList.value = res.data
|
||||
loading.value = false
|
||||
}
|
||||
getAppList()
|
||||
|
||||
const toLink = (addon: string) => {
|
||||
addonIndexRoute[addon] && router.push({ name: addonIndexRoute[addon] })
|
||||
const toLink = (item: any) => {
|
||||
console.log('tol', item)
|
||||
if (item.url) {
|
||||
router.push(item.url)
|
||||
} else {
|
||||
addonIndexRoute[item.key] && router.push({ name: addonIndexRoute[item.key] })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup async>
|
||||
import { ref, reactive, computed, watch, toRaw } from 'vue'
|
||||
import { ref, reactive, computed, watch, toRaw, nextTick } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { addRole, editRole, getRoleInfo, getSiteMenus } from '@/app/api/sys'
|
||||
@ -60,11 +60,13 @@ const selectAll = ref(false)
|
||||
const checkStrictly = ref(false)
|
||||
const treeRef: Record<string, any> | null = ref(null)
|
||||
watch(selectAll, () => {
|
||||
if (selectAll.value) {
|
||||
treeRef.value.setCheckedNodes(toRaw(menus.value))
|
||||
} else {
|
||||
treeRef.value.setCheckedNodes([])
|
||||
}
|
||||
nextTick(() => {
|
||||
if (selectAll.value) {
|
||||
treeRef.value.setCheckedNodes(toRaw(menus.value))
|
||||
} else {
|
||||
treeRef.value.setCheckedKeys([])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const handleCheckChange = debounce((e) => {
|
||||
|
||||
@ -16,13 +16,13 @@
|
||||
<el-switch v-model="formData.is_open"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('PCDomainName')">
|
||||
<el-form-item :label="t('pCDomainName')">
|
||||
<el-input :model-value="formData.request_url" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.request_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="ml-2 cursor-pointer visit-btn">{{t('clickVisit')}}</span>
|
||||
<span class="ml-2 cursor-pointer visit-btn" @click="visitFn">{{t('clickVisit')}}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
@ -17,6 +17,9 @@
|
||||
<el-button type="primary" @click="insert" :loading="uploading" :disabled="loading">{{ t('cloudRelease') }}</el-button>
|
||||
<el-button @click="localInsert" :disabled="loading">{{ t('localRelease') }}</el-button>
|
||||
</div>
|
||||
<div class="mt-[20px]" v-else>
|
||||
<el-button type="primary" @click="againUpload" :loading="uploading" :disabled="loading">{{ t('uploadWeapp') }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-table class="mt-[15px]" :data="weappTableData.data" v-loading="weappTableData.loading" size="default">
|
||||
<template #empty>
|
||||
@ -55,13 +58,13 @@
|
||||
<el-dialog v-model="dialogVisible" :title="t('codeDownTwoDesc')" width="30%" :before-close="handleClose">
|
||||
<el-form ref="ruleFormRef" :model="form" label-width="120px">
|
||||
<el-form-item prop="code" :label="t('code')">
|
||||
<el-input v-model="form.code" :placeholder="t('codePlaceholder')" onkeyup="this.value = this.value.replace(/[^\d\.]/g,'');" />
|
||||
<el-input v-model.trim="form.code" :placeholder="t('codePlaceholder')" onkeyup="this.value = this.value.replace(/[^\d\.]/g,'');" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="path" :label="t('path')">
|
||||
<upload-file v-model="form.path" :api="'weapp/upload'" :accept="'.zip'" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('content')">
|
||||
<el-input type="textarea" v-model="form.content" :placeholder="t('contentPlaceholder')" />
|
||||
<el-input type="textarea" v-model.trim="form.content" :placeholder="t('contentPlaceholder')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@ -75,8 +78,8 @@
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="failReasonDialogVisible" :title="t('failReason')" width="60%">
|
||||
<el-scrollbar class="h-[60vh] w-full whitespace-pre p-[20px]">
|
||||
{{ failReason }}
|
||||
<el-scrollbar class="h-[60vh] w-full whitespace-pre-wrap p-[20px]">
|
||||
<div v-html="failReason"></div>
|
||||
</el-scrollbar>
|
||||
</el-dialog>
|
||||
|
||||
@ -279,7 +282,6 @@ const configElMessageBox = () => {
|
||||
}
|
||||
).then(() => {
|
||||
router.push({ path: '/channel/weapp/config' })
|
||||
}).catch((action: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
164
admin/src/app/views/channel/weapp/components/modify-domain.vue
Normal file
@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('functionSetting')" width="700px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="180px" ref="formRef" :rules="formRules" class="page-form pr-[100px]" v-loading="loading">
|
||||
<el-form-item :label="t('requestUrl')" prop="requestdomain">
|
||||
<el-input v-model="formData.requestdomain" :placeholder="t('requestdomainPlaceholder')" type="textarea">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('socketUrl')" prop="wsrequestdomain">
|
||||
<el-input v-model="formData.wsrequestdomain" :placeholder="t('wsrequestdomainPlaceholder')" type="textarea">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('uploadUrl')" prop="uploaddomain">
|
||||
<el-input v-model="formData.uploaddomain" :placeholder="t('uploaddomainPlaceholder')" type="textarea">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('downloadUrl')" prop="downloaddomain">
|
||||
<el-input v-model="formData.downloaddomain" :placeholder="t('downloaddomainPlaceholder')" type="textarea">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('udpUrl')" prop="udpdomain">
|
||||
<el-input v-model="formData.udpdomain" :placeholder="t('udpdomainPlaceholder')" type="textarea">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('tcpUrl')" prop="tcpdomain">
|
||||
<el-input v-model="formData.tcpdomain" :placeholder="t('tcpdomainPlaceholder')" type="textarea">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, reactive, ref} from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { setWeappDomain } from '@/app/api/weapp'
|
||||
import Test from '@/utils/test'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
requestdomain: '',
|
||||
wsrequestdomain: '',
|
||||
uploaddomain: '',
|
||||
downloaddomain: '',
|
||||
tcpdomain: '',
|
||||
udpdomain: ''
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
requestdomain: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validatorProtocol(rule, value, callback, 'https://'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
uploaddomain: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validatorProtocol(rule, value, callback, 'https://'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
downloaddomain: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validatorProtocol(rule, value, callback, 'https://'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
wsrequestdomain: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validatorProtocol(rule, value, callback, 'wss://'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
tcpdomain: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validatorProtocol(rule, value, callback, 'tcp://'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
udpdomain: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validatorProtocol(rule, value, callback, 'udp://'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const validatorProtocol = (rule: any, value: any, callback: any, protocol: string) => {
|
||||
if (!Test.empty(value)) {
|
||||
let flag = true
|
||||
value.split(';').forEach((item: string) => {
|
||||
if (!item.startsWith(protocol)) {
|
||||
flag = false
|
||||
callback(new Error(t('domainError')))
|
||||
}
|
||||
})
|
||||
if (flag) callback()
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
|
||||
const data = formData
|
||||
|
||||
setWeappDomain(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete', data)
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setFormData = async (data: any = null) => {
|
||||
loading.value = false
|
||||
Object.assign(formData, initialFormData)
|
||||
if (data) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('privacyAgreementTitle')" width="900px" :destroy-on-close="true">
|
||||
<div class="h-[60vh]">
|
||||
<el-scrollbar>
|
||||
<el-form :model="formData" label-width="auto" label-position="left" ref="formRef" :rules="formRules"
|
||||
class="page-form w-[700px] mx-auto" v-loading="loading">
|
||||
<h3 class="text-center text-xl font-bold my-[20px]">{{ config.weapp_name }} 小程序隐私保护指引</h3>
|
||||
<h4 class="text-lg my-[10px]">1. 开发者处理的信息</h4>
|
||||
<div class="mb-[8px]">根据法律规定,开发者仅处理实现小程序功能所必要的信息。</div>
|
||||
<div class="setting-list">
|
||||
<setting-list v-model="formData.setting_list" ref="settingListRef"/>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" link @click="settingListRef.addSettingList()">{{ t('addSettingType') }}</el-button>
|
||||
</div>
|
||||
<h4 class="text-lg my-[10px]">2. 第三方插件信息/SDK信息</h4>
|
||||
<div class="mb-[8px]">
|
||||
为实现特定功能,开发者可能会接入由第三方提供的插件/SDK。第三方插件/SDK的个人信息处理规则,请以其公示的官方说明为准。{{ config.weapp_name }}小程序接入的第三方插件信息/SDK信息如下:
|
||||
</div>
|
||||
<div>
|
||||
<div v-for="(item, index) in formData.sdk_privacy_info_list" class="mb-[15px]">
|
||||
<el-form-item label="SDK名称" class="!mb-[8px]">
|
||||
<el-input v-model="formData.sdk_privacy_info_list[index].sdk_name" class="input-width" placeholder="请输入SDK名称" />
|
||||
<el-button type="primary" class="ml-[10px]" link @click="delSdk(index)">{{ t('delete') }}</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="SDK提供方名称" class="!mb-[8px]">
|
||||
<el-input v-model="formData.sdk_privacy_info_list[index].sdk_biz_name" class="input-width" placeholder="请输入SDK提供方名称" />
|
||||
</el-form-item>
|
||||
<setting-list v-model="formData.sdk_privacy_info_list[index].sdk_list" ref="sdkSettingListRef"/>
|
||||
<el-form-item label="" class="!mb-[8px]">
|
||||
<el-button type="primary" link @click="sdkSettingListRef[index].addSettingList()">{{ t('addSdkSettingList') }}</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" link @click="addSdk">{{ t('addSdkInfo') }}</el-button>
|
||||
</div>
|
||||
<h4 class="text-lg my-[10px]">3. 你的权益</h4>
|
||||
<div class="mb-[8px]">3.1
|
||||
关于收集你的位置信息,你可以通过以下路径:小程序主页右上角“…”—“设置”—点击特定信息—点击“不允许”,撤回对开发者的授权。</div>
|
||||
<div class="mb-[8px]">3.2 关于收集你的微信昵称、头像、收集你的手机号,你可以通过以下路径:小程序主页右上角“...” — “设置” —
|
||||
“小程序已获取的信息” — 点击特定信息 —
|
||||
点击“通知开发者删除”,开发者承诺收到通知后将删除信息。法律法规另有规定的,开发者承诺将停止除存储和采取必要的安全保护措施之外的处理。</div>
|
||||
<div class="mb-[8px]">3.3 关于你的个人信息,你可以通过以下方式与开发者联系,行使查阅、复制、更正、删除等法定权利。</div>
|
||||
<div class="mb-[8px]">3.4
|
||||
若你在小程序中注册了账号,你可以通过以下方式与开发者联系,申请注销你在小程序中使用的账号。在受理你的申请后,开发者承诺在十五个工作日内完成核查和处理,并按照法律法规要求处理你的相关信息。</div>
|
||||
<div>
|
||||
<el-form-item label="电话" class="!mb-[8px]">
|
||||
<el-input v-model="formData.owner_setting.contact_phone" class="input-width" placeholder="请输入开发者的手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" class="!mb-[8px]">
|
||||
<el-input v-model="formData.owner_setting.contact_email" class="input-width" placeholder="请输入开发者的邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="微信号" class="!mb-[8px]">
|
||||
<el-input v-model="formData.owner_setting.contact_weixin" class="input-width" placeholder="请输入开发者的微信号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="qq号" class="!mb-[8px]">
|
||||
<el-input v-model="formData.owner_setting.contact_qq" class="input-width" placeholder="请输入开发者的qq号" />
|
||||
</el-form-item>
|
||||
<div class="form-tip">信息收集方(开发者)的联系方式,4种联系方式至少要填一种</div>
|
||||
</div>
|
||||
<h4 class="text-lg my-[10px]">4. 开发者对信息的存储</h4>
|
||||
<div>
|
||||
<el-radio-group v-model="formData.store_expire_type">
|
||||
<div>
|
||||
<el-radio :label="1">
|
||||
固定存储期限
|
||||
<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="formData.store_expire_timestamp"/></div>
|
||||
</el-radio>
|
||||
</div>
|
||||
<div>
|
||||
<el-radio :label="0">开发者承诺,除法律法规另有规定外,开发者对你的信息的保存期限应当为实现处理目的所必要的最短时间。</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<h4 class="text-lg my-[10px]">5. 信息的使用规则</h4>
|
||||
<div class="mb-[8px]">5.1 开发者将会在本指引所明示的用途内使用收集的信息</div>
|
||||
<div class="mb-[8px]">
|
||||
5.2 如开发者使用你的信息超出本指引目的或合理范围,开发者必须在变更使用目的或范围前,再次以
|
||||
<div class="!w-[180px] inline-block">
|
||||
<el-input v-model="formData.owner_setting.notice_method"/>
|
||||
</div>
|
||||
方式告知并征得你的明示同意。</div>
|
||||
<h4 class="text-lg my-[10px]">6. 信息对外提供</h4>
|
||||
<div class="mb-[8px]">6.1
|
||||
开发者承诺,不会主动共享或转让你的信息至任何第三方,如存在确需共享或转让时,开发者应当直接征得或确认第三方征得你的单独同意。</div>
|
||||
<div class="mb-[8px]">6.2
|
||||
开发者承诺,不会对外公开披露你的信息,如必须公开披露时,开发者应当向你告知公开披露的目的、披露信息的类型及可能涉及的信息,并征得你的单独同意。</div>
|
||||
<h4 class="text-lg my-[10px]">7.
|
||||
你认为开发者未遵守上述约定,或有其他的投诉建议、或未成年人个人信息保护相关问题,可通过以下方式与开发者联系;或者向微信进行投诉。</h4>
|
||||
<!-- <h4 class="text-lg my-[10px]">8. 补充文档</h4>-->
|
||||
</el-form>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import Test from '@/utils/test'
|
||||
import SettingList from './setting-list.vue'
|
||||
import { getWeappPrivacySetting, setWeappPrivacySetting } from '@/app/api/weapp'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
const settingListRef = ref(null)
|
||||
const sdkSettingListRef = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
setting_list: [
|
||||
{
|
||||
privacy_key: 'UserInfo',
|
||||
privacy_text: ''
|
||||
},
|
||||
{
|
||||
privacy_key: 'Location',
|
||||
privacy_text: ''
|
||||
},
|
||||
{
|
||||
privacy_key: 'PhoneNumber',
|
||||
privacy_text: ''
|
||||
}
|
||||
],
|
||||
owner_setting: {
|
||||
notice_method: ''
|
||||
},
|
||||
sdk_privacy_info_list: [],
|
||||
store_expire_type: 0,
|
||||
store_expire_timestamp: ''
|
||||
}
|
||||
|
||||
const formData: Record<string, any> = reactive({...initialFormData})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
const formRules = computed(() => {
|
||||
return {}
|
||||
})
|
||||
|
||||
const addSdk = () => {
|
||||
formData.sdk_privacy_info_list.push({
|
||||
sdk_name: '',
|
||||
sdk_biz_name: '',
|
||||
sdk_list: []
|
||||
})
|
||||
}
|
||||
|
||||
const delSdk = (index: number) => {
|
||||
formData.sdk_privacy_info_list.splice(index, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
|
||||
const data = formData
|
||||
|
||||
if (!data.store_expire_type) data.owner_setting.store_expire_timestamp = data.store_expire_timestamp
|
||||
|
||||
setWeappPrivacySetting(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete', data)
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setFormData = async () => {
|
||||
getWeappPrivacySetting().then(({ data }) => {
|
||||
loading.value = false
|
||||
Object.assign(formData, initialFormData)
|
||||
if (data) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
if (data.owner_setting.store_expire_timestamp) {
|
||||
formData.store_expire_type = 1
|
||||
formData.store_expire_timestamp = data.owner_setting.store_expire_timestamp
|
||||
}
|
||||
}
|
||||
showDialog.value = true
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
221
admin/src/app/views/channel/weapp/components/setting-list.vue
Normal file
@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div v-for="(item, index) in value">
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'UserInfo'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,收集你的微信昵称、头像。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Location'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,收集你的位置信息。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'PhoneNumber'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,收集你的手机号。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Address'">
|
||||
开发者收集你的地址,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Record'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,访问你的麦克风。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Contact'">
|
||||
开发者使用你的通讯录(仅写入)权限,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'EXOrderInfo'">
|
||||
开发者收集你的订单信息,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'EXUserOpLog'">
|
||||
开发者收集你的操作日志,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'BlueTooth'">
|
||||
开发者访问你的蓝牙,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'MessageFile'">
|
||||
开发者收集你选中的文件,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Compass'">
|
||||
开发者调用你的磁场传感器,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Clipboard'">
|
||||
开发者读取你的剪切板,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'DeviceMotion'">
|
||||
开发者调用你的方向传感器,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'ChooseLocation'">
|
||||
开发者获取你选择的位置信息,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'CalendarWriteOnly'">
|
||||
开发者使用你的日历(仅写入)权限,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'AlbumWriteOnly'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div> ,开发者将在获取你的明示同意后,使用你的相册(仅写入)权限。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'EXUserPublishContent'">
|
||||
开发者收集你的发布内容,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'DeviceInfo'">
|
||||
开发者收集你的设备信息,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Album'">
|
||||
开发者收集你选中的照片或视频信息,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Invoice'">
|
||||
开发者收集你的发票信息,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'RunData'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,收集你的微信运动步数。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Camera'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,访问你的摄像头。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'EXUserFollowAcct'">
|
||||
开发者收集你的所关注账号,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'EXIDNumber'">
|
||||
开发者收集你的身份证号码,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'LicensePlate'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,收集你的车牌号。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Email'">
|
||||
开发者收集你的邮箱,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Accelerometer'">
|
||||
开发者调用你的加速传感器,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Gyroscope'">
|
||||
开发者调用你的陀螺仪传感器,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="settingTypeDialog" :title="t('settingTypeTitle')" width="500px" :destroy-on-close="true">
|
||||
<el-checkbox-group v-model="checkList">
|
||||
<template v-for="(item, index) in privacyList">
|
||||
<el-checkbox :label="item.privacy_key" v-if="!checkIsSelected(item.privacy_key)">{{ item.privacy_text }}</el-checkbox>
|
||||
</template>
|
||||
</el-checkbox-group>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="settingTypeDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="selectSettingType()">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const settingTypeDialog = ref(false)
|
||||
|
||||
const privacyList = ref([
|
||||
{ privacy_key: 'UserInfo', privacy_text: '用户信息(微信昵称、头像)' },
|
||||
{ privacy_key: 'Location', privacy_text: '位置信息' },
|
||||
{ privacy_key: 'Address', privacy_text: '地址' },
|
||||
{ privacy_key: 'Invoice', privacy_text: '发票信息' },
|
||||
{ privacy_key: 'RunData', privacy_text: '微信运动数据' },
|
||||
{ privacy_key: 'Record', privacy_text: '麦克风' },
|
||||
{ privacy_key: 'Album', privacy_text: '选中的照片或视频信息' },
|
||||
{ privacy_key: 'Camera', privacy_text: '摄像头' },
|
||||
{ privacy_key: 'PhoneNumber', privacy_text: '手机号码' },
|
||||
{ privacy_key: 'Contact', privacy_text: '通讯录(仅写入)权限' },
|
||||
{ privacy_key: 'DeviceInfo', privacy_text: '设备信息' },
|
||||
{ privacy_key: 'EXIDNumber', privacy_text: '身份证号码' },
|
||||
{ privacy_key: 'EXOrderInfo', privacy_text: '订单信息' },
|
||||
{ privacy_key: 'EXUserPublishContent', privacy_text: '发布内容' },
|
||||
{ privacy_key: 'EXUserFollowAcct', privacy_text: '所关注账号' },
|
||||
{ privacy_key: 'EXUserOpLog', privacy_text: '操作日志' },
|
||||
{ privacy_key: 'AlbumWriteOnly', privacy_text: '相册(仅写入)权限' },
|
||||
{ privacy_key: 'LicensePlate', privacy_text: '车牌号' },
|
||||
{ privacy_key: 'BlueTooth', privacy_text: '蓝牙' },
|
||||
{ privacy_key: 'CalendarWriteOnly', privacy_text: '日历(仅写入)权限' },
|
||||
{ privacy_key: 'Email', privacy_text: '邮箱' },
|
||||
{ privacy_key: 'MessageFile', privacy_text: '选中的文件' },
|
||||
{ privacy_key: 'ChooseLocation', privacy_text: '选择的位置信息' },
|
||||
{ privacy_key: 'Accelerometer', privacy_text: '加速传感器' },
|
||||
{ privacy_key: 'Compass', privacy_text: '磁场传感器' },
|
||||
{ privacy_key: 'DeviceMotion', privacy_text: '方向传感器' },
|
||||
{ privacy_key: 'Gyroscope', privacy_text: '陀螺仪传感器' },
|
||||
{ privacy_key: 'Clipboard', privacy_text: '剪切板' }
|
||||
])
|
||||
|
||||
const emits = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const value = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emits('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const removeSettingList = (index: number) => {
|
||||
value.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const checkList = ref([])
|
||||
const selectSettingType = () => {
|
||||
checkList.value.forEach((item: string) => {
|
||||
value.value.push({
|
||||
privacy_key: item,
|
||||
privacy_text: ''
|
||||
})
|
||||
})
|
||||
settingTypeDialog.value = false
|
||||
checkList.value = []
|
||||
}
|
||||
|
||||
const addSettingList = () => {
|
||||
settingTypeDialog.value = true
|
||||
}
|
||||
|
||||
const selectedSettingType = computed(() => {
|
||||
return value.value.map((item: any) => item.privacy_key)
|
||||
})
|
||||
const checkIsSelected = (key: string) => {
|
||||
return selectedSettingType.value.includes(key)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
addSettingList
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@ -3,7 +3,7 @@
|
||||
<div class="main-container">
|
||||
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
<el-page-header :content="pageName" :icon="ArrowLeft" @back="$router.back()" />
|
||||
<el-page-header :content="pageName" :icon="ArrowLeft" @back="back()" />
|
||||
</el-card>
|
||||
|
||||
<el-form class="page-form mt-[15px]" :model="formData" label-width="170px" ref="formRef" :rules="formRules" v-loading="loading">
|
||||
@ -50,7 +50,7 @@
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never">
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never" v-show="!formData.is_authorization">
|
||||
<h3 class="panel-title !text-sm">{{ t('theServerSetting') }}</h3>
|
||||
|
||||
<el-form-item label="URL">
|
||||
@ -84,37 +84,78 @@
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never">
|
||||
<div class="flex">
|
||||
<div class="flex items-start justify-between">
|
||||
<h3 class="panel-title !text-sm">{{ t('functionSetting') }}</h3>
|
||||
<el-button type="primary" link @click="modifyDomainFn" v-if="formData.is_authorization">{{ t('update') }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('requestUrl')">
|
||||
<el-input :model-value="formData.request_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.request_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('socketUrl')">
|
||||
<el-input :model-value="formData.socket_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.socket_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('uploadUrl')">
|
||||
<el-input :model-value="formData.upload_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.upload_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('downloadUrl')">
|
||||
<el-input :model-value="formData.download_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.download_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<div v-if="!formData.is_authorization">
|
||||
<el-form-item :label="t('requestUrl')">
|
||||
<el-input :model-value="formData.request_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.request_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('socketUrl')">
|
||||
<el-input :model-value="formData.socket_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.socket_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('uploadUrl')">
|
||||
<el-input :model-value="formData.upload_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.upload_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('downloadUrl')">
|
||||
<el-input :model-value="formData.download_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.download_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-form-item :label="t('requestUrl')">
|
||||
<div v-if="formData.domain.requestdomain">{{ formData.domain.requestdomain }}</div>
|
||||
<div v-else>-</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('socketUrl')">
|
||||
<div v-if="formData.domain.wsrequestdomain">{{ formData.domain.wsrequestdomain }}</div>
|
||||
<div v-else>-</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('uploadUrl')">
|
||||
<div v-if="formData.domain.uploaddomain">{{ formData.domain.uploaddomain }}</div>
|
||||
<div v-else>-</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('downloadUrl')">
|
||||
<div v-if="formData.domain.downloaddomain">{{ formData.domain.downloaddomain }}</div>
|
||||
<div v-else>-</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('udpUrl')">
|
||||
<div v-if="formData.domain.udpdomain">{{ formData.domain.udpdomain }}</div>
|
||||
<div v-else>-</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('tcpUrl')">
|
||||
<div v-if="formData.domain.tcpdomain">{{ formData.domain.tcpdomain }}</div>
|
||||
<div v-else>-</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never" v-if="formData.is_authorization">
|
||||
<div class="flex items-start justify-between">
|
||||
<h3 class="panel-title !text-sm">{{ t('serviceContentStatement') }}</h3>
|
||||
</div>
|
||||
<el-form-item :label="t('privacyAgreement')">
|
||||
<div class="flex items-center">
|
||||
<div class="form-tip !mt-0">{{ t('privacyAgreementTips') }}</div>
|
||||
<el-button type="primary" link @click="modifyPrivacyAgreementFn">{{ t('setting') }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
</el-form>
|
||||
@ -125,6 +166,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<modify-domain ref="modifyDomainRef" @complete="modifyDomainComplete"/>
|
||||
<modify-privacy-agreement :config="formData" ref="modifyPrivacyAgreementRef"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -132,14 +176,18 @@ import { reactive, ref, watch, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getWeappConfig, setWeappConfig } from '@/app/api/weapp'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { ElMessage, FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage, FormInstance } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ModifyDomain from '@/app/views/channel/weapp/components/modify-domain.vue'
|
||||
import ModifyPrivacyAgreement from '@/app/views/channel/weapp/components/modify-privacy-agreement.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const back = () => {
|
||||
router.push('/channel/weapp')
|
||||
}
|
||||
const loading = ref(true)
|
||||
|
||||
const formData = reactive<Record<string, any>>({
|
||||
@ -157,7 +205,15 @@ const formData = reactive<Record<string, any>>({
|
||||
upload_url: '',
|
||||
download_url: '',
|
||||
upload_private_key: '',
|
||||
is_authorization: 0
|
||||
is_authorization: 0,
|
||||
domain: {
|
||||
requestdomain: '',
|
||||
wsrequestdomain: '',
|
||||
uploaddomain: '',
|
||||
downloaddomain: '',
|
||||
tcpdomain: '',
|
||||
udpdomain: ''
|
||||
}
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
@ -178,10 +234,10 @@ const formRules = computed(() => {
|
||||
{ required: !formData.is_authorization, message: t('appSecretPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
token: [
|
||||
{ required: true, message: t('tokenPlaceholder'), trigger: 'blur' }
|
||||
{ required: !formData.is_authorization, message: t('tokenPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
encoding_aes_key: [
|
||||
{ required: true, message: t('encodingAesKeyPlaceholder'), trigger: 'blur' }
|
||||
{ required: !formData.is_authorization, message: t('encodingAesKeyPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
})
|
||||
@ -240,6 +296,20 @@ const save = async (formEl: FormInstance | undefined) => {
|
||||
})
|
||||
}
|
||||
|
||||
const modifyDomainRef = ref(null)
|
||||
const modifyDomainFn = () => {
|
||||
modifyDomainRef.value.setFormData(formData.domain)
|
||||
modifyDomainRef.value.showDialog = true
|
||||
}
|
||||
|
||||
const modifyDomainComplete = (data) => {
|
||||
formData.domain = data
|
||||
}
|
||||
|
||||
const modifyPrivacyAgreementRef = ref(null)
|
||||
const modifyPrivacyAgreementFn = () => {
|
||||
modifyPrivacyAgreementRef.value.setFormData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@ -138,7 +138,7 @@
|
||||
|
||||
<div class="edit-component-wrap">
|
||||
|
||||
<component v-if="diyStore.currentComponent" :is="modules[diyStore.currentComponent]" :value="diyStore.value[diyStore.currentIndex]">
|
||||
<component v-if="diyStore.currentComponent" :is="modules[diyStore.currentComponent]" :key="diyStore.currentIndex" :value="diyStore.value[diyStore.currentIndex]">
|
||||
<template #style>
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('componentStyleTitle') }}</h3>
|
||||
@ -300,9 +300,6 @@ const modulesFiles = import.meta.glob('./components/*.vue', { eager: true })
|
||||
const addonModulesFiles = import.meta.glob('@/addon/**/views/diy/components/*.vue', { eager: true })
|
||||
addonModulesFiles && Object.assign(modulesFiles, addonModulesFiles)
|
||||
|
||||
// todo 考虑用一个编辑页面实现,方便后期维护,根据路由判断,是微页面还是系统表单
|
||||
// todo 系统表单可以使用自定义组件,微页面不能用系统表单组件
|
||||
|
||||
const modules = {}
|
||||
for (const [key, value] of Object.entries(modulesFiles)) {
|
||||
const moduleName = key.split('/').pop()
|
||||
|
||||
@ -127,7 +127,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getApps,getDiyPageList, deleteDiyPage, getDiyTemplate, editDiyPageShare, setUseDiyPage } from '@/app/api/diy'
|
||||
import { getApps,getDiyPageList, deleteDiyPage, getDiyTemplate, editDiyPageShare, setUseDiyPage, copyDiy } from '@/app/api/diy'
|
||||
import { ElMessageBox, FormInstance } from 'element-plus'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { setTablePageStorage,getTablePageStorage } from "@/utils/common";
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-image v-for="(item,index) in props.data.handle_field_value" :src="img(item)" class="w-[70px] h-[70px]" :class="{ 'mr-[5px]' : (index + 1) < props.data.handle_field_value.length }" fit="contain" :preview-src-list="imgList" :zoom-rate="1.2" :max-scale="7"
|
||||
:min-scale="0.2" :initial-index="index" :hide-on-click-modal="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { img } from '@/utils/common'
|
||||
import { t } from "@/lang";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
const imgList = computed(() => {
|
||||
return props.data.handle_field_value.map((item: any) => {
|
||||
return img(item)
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="form-render">{{ props.data.render_value }}</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { t } from "@/lang";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-render {
|
||||
word-wrap: break-word; /* 长单词或长字符串自动换行 */
|
||||
word-break: break-word; /* 强制断词换行 */
|
||||
white-space: pre-wrap; /* 保持空格和换行 */
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('地址格式')">
|
||||
<el-radio-group v-model="diyStore.editComponent.addressFormat" class="!block">
|
||||
<el-radio class="!block" label="province/city/district/address">{{ t('省/市/区/街道/详细地址') }}</el-radio>
|
||||
<el-radio class="!block" label="province/city/district/street">{{ t('省/市/区/街道(镇)') }}</el-radio>
|
||||
<el-radio class="!block" label="province/city/district">{{ t('省/市/区(县)') }}</el-radio>
|
||||
<el-radio class="!block" label="province/city">{{ t('省/市') }}</el-radio>
|
||||
<el-radio class="!block" label="province">{{ t('省') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item class="display-block">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('隐私保护') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>会自动将提交的个人信息做加密展示。</p>
|
||||
<p>适用于公开展示收集的数据且不暴露用户隐私。</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
|
||||
<div class="text-sm text-gray-400">{{ t('提交后自动隐藏地址,仅管理员可查看') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
return res
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
194
admin/src/app/views/diy_form/components/edit-form-checkbox.vue
Normal file
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('style')">
|
||||
<el-radio-group v-model="diyStore.editComponent.style">
|
||||
<el-radio label="style-1">{{ t('defaultSources') }}</el-radio>
|
||||
<el-radio label="style-2">{{ t('listStyle') }}</el-radio>
|
||||
<el-radio label="style-3">{{ t('dropDownStyle') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('option')">
|
||||
<div ref="formCheckboxRef">
|
||||
<div v-for="(option, index) in diyStore.editComponent.options" :key="option.id" class="option-item flex items-center mb-[15px]">
|
||||
<el-input v-model="diyStore.editComponent.options[index].text" class="!w-[215px]" :placeholder="t('optionPlaceholder')" maxlength="30" clearable />
|
||||
<span v-if="diyStore.editComponent.options.length > 1" @click="removeOption(index)" class="cursor-pointer ml-[5px] nc-iconfont nc-icon-shanchu-yuangaizhiV6xx"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-primary cursor-pointer mr-[10px]" @click="addOption">{{ t('addSingleOption') }}</span>
|
||||
<el-popover :visible="visible" placement="bottom" :width="300">
|
||||
<p class="mb-[5px]">{{ t('addMultipleOption') }}</p>
|
||||
<p class="text-[#888] text-[12px] mb-[5px]">{{ t('addOptionTips') }}</p>
|
||||
<el-input v-model.trim="optionsValue" type="textarea" clearable maxlength="200" show-word-limit />
|
||||
<div class="mt-[10px] text-right">
|
||||
<el-button size="small" text @click="visible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button size="small" type="primary" @click="batchAddOptions">{{ t('confirm') }}</el-button>
|
||||
</div>
|
||||
<template #reference>
|
||||
<span class="text-primary cursor-pointer" @click="visible = true">{{ t('addMultipleOption') }}</span>
|
||||
</template>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<!-- <el-form label-width="100px" class="px-[10px]">-->
|
||||
<!-- <el-form-item class="display-block">-->
|
||||
<!-- <template #label>-->
|
||||
<!-- <div class="flex items-center">-->
|
||||
<!-- <span class="mr-[3px]">{{ t('隐私保护') }}</span>-->
|
||||
<!-- <el-tooltip effect="light" placement="top">-->
|
||||
<!-- <template #content>-->
|
||||
<!-- <p>会自动将提交的个人信息做加密展示。</p>-->
|
||||
<!-- <p>适用于公开展示收集的数据且不暴露用户隐私。</p>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <QuestionFilled color="#999999" />-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-switch v-model="diyStore.editComponent.field.privacyProtection" />-->
|
||||
<!-- <div class="text-sm text-gray-400">{{ t('提交后自动隐藏内容,仅管理员可查看') }}</div>-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<!-- </el-form>-->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, onMounted, nextTick} from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import Sortable from 'sortablejs'
|
||||
import { range } from 'lodash-es'
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
let pass = true;
|
||||
for (let i = 0; i < diyStore.value[index].options.length; i++) {
|
||||
if (!diyStore.value[index].options[i].text) {
|
||||
res.code = false;
|
||||
res.message = t('optionPlaceholder');
|
||||
pass = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pass) return res;
|
||||
|
||||
let uniqueOptions = uniqueByKey(diyStore.value[index].options, 'text')
|
||||
if (uniqueOptions.length != diyStore.value[index].options.length) {
|
||||
res.code = false;
|
||||
res.message = t('errorTipsOne')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
diyStore.editComponent.options.forEach((item: any) => {
|
||||
if (!item.id) item.id = diyStore.generateRandom()
|
||||
})
|
||||
|
||||
const visible = ref(false)
|
||||
const optionsValue = ref()
|
||||
const addOption = () => {
|
||||
diyStore.editComponent.options.push({
|
||||
id: diyStore.generateRandom(),
|
||||
text: '选项' + (diyStore.editComponent.options.length + 1)
|
||||
});
|
||||
};
|
||||
|
||||
const removeOption = (index:any) => {
|
||||
diyStore.editComponent.options.splice(index, 1);
|
||||
}
|
||||
|
||||
// 批量添加选项
|
||||
const batchAddOptions = () => {
|
||||
if (optionsValue.value.trim()) {
|
||||
const newOptions = optionsValue.value.split(',').map((option: any) => {
|
||||
return {
|
||||
id: diyStore.generateRandom(),
|
||||
text: option.trim()
|
||||
};
|
||||
}).filter((option: any) => option.text !== '');
|
||||
|
||||
// 去除重复的选项
|
||||
const uniqueNewOptions = uniqueByKey(newOptions, 'text');
|
||||
|
||||
// 过滤掉已存在的选项
|
||||
const filteredNewOptions = uniqueNewOptions.filter((newOption: any) =>
|
||||
!diyStore.editComponent.options.some((existingOption: any) => existingOption.text === newOption.text)
|
||||
);
|
||||
|
||||
// 如果有新的选项,添加到选项列表中
|
||||
if (filteredNewOptions.length > 0) {
|
||||
diyStore.editComponent.options.push(...filteredNewOptions);
|
||||
} else {
|
||||
ElMessage({
|
||||
message: t('errorTipsTwo'),
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
optionsValue.value = '';
|
||||
visible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 数组去重
|
||||
const uniqueByKey = (arr: any, key: any) => {
|
||||
const seen = new Set();
|
||||
return arr.filter((item: any) => {
|
||||
const serializedKey = JSON.stringify(item[key]);
|
||||
return seen.has(serializedKey) ? false : seen.add(serializedKey);
|
||||
});
|
||||
}
|
||||
|
||||
const formCheckboxRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const sortable = Sortable.create(formCheckboxRef.value, {
|
||||
group: 'option-item',
|
||||
animation: 200,
|
||||
onEnd: event => {
|
||||
const temp = diyStore.editComponent.options[event.oldIndex!]
|
||||
diyStore.editComponent.options.splice(event.oldIndex!, 1)
|
||||
diyStore.editComponent.options.splice(event.newIndex!, 0, temp)
|
||||
sortable.sort(
|
||||
range(diyStore.editComponent.options.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
205
admin/src/app/views/diy_form/components/edit-form-date-scope.vue
Normal file
@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('dataFormat')">
|
||||
<el-radio-group v-model="diyStore.editComponent.dateFormat">
|
||||
<div class="flex flex-col">
|
||||
<el-radio label="YYYY年M月D日">{{ dateFormat.format1 }}</el-radio>
|
||||
<el-radio label="YYYY-MM-DD">{{ dateFormat.format2 }}</el-radio>
|
||||
<el-radio label="YYYY/MM/DD">{{ dateFormat.format3 }}</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('startDate') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.start.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('defaultValue')">
|
||||
<el-switch v-model="diyStore.editComponent.start.defaultControl" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.start.defaultControl">
|
||||
<el-radio-group v-model="diyStore.editComponent.start.dateWay">
|
||||
<el-radio label="current">{{ t('currentDate') }}</el-radio>
|
||||
<el-radio label="diy">{{ t('diyDate') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.start.defaultControl && diyStore.editComponent.start.dateWay == 'diy'">
|
||||
<el-date-picker v-model="diyStore.editComponent.field.default.start.date" format="YYYY/MM/DD" value-format="YYYY-MM-DD" type="date" :placeholder="t('startDataPlaceholder')" @change="startDateChange" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('endDate') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.end.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('defaultValue')">
|
||||
<el-switch v-model="diyStore.editComponent.end.defaultControl" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.end.defaultControl">
|
||||
<el-radio-group v-model="diyStore.editComponent.end.dateWay">
|
||||
<el-radio label="current">{{ t('currentDate') }}</el-radio>
|
||||
<el-radio label="diy">{{ t('diyDate') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.end.defaultControl && diyStore.editComponent.end.dateWay == 'diy'">
|
||||
<el-date-picker :disabled-date="disabledEndDate" v-model="diyStore.editComponent.field.default.end.date" format="YYYY/MM/DD" value-format="YYYY-MM-DD" type="date" :placeholder="t('endDataPlaceholder')" @change="endDateChange" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('textStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('textFontSize')">
|
||||
<el-slider v-model="diyStore.editComponent.fontSize" show-input size="small" class="ml-[10px] diy-nav-slider" :min="12" :max="18" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textFontWeight')">
|
||||
<el-radio-group v-model="diyStore.editComponent.fontWeight">
|
||||
<el-radio :label="'normal'">{{ t('fontWeightNormal') }}</el-radio>
|
||||
<el-radio :label="'bold'">{{ t('fontWeightBold') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.textColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { timeTurnTimeStamp } from '@/utils/common'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
let starTime = diyStore.value[index].field.default.start.date
|
||||
let endTime = diyStore.value[index].field.default.end.date
|
||||
|
||||
let today = new Date()
|
||||
const hours = String(today.getHours()).padStart(2, '0')
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0')
|
||||
|
||||
if (diyStore.editComponent.start.dateWay == 'current') {
|
||||
starTime = today.toISOString().split('T')[0]
|
||||
}
|
||||
if (diyStore.editComponent.end.dateWay == 'current') {
|
||||
endTime = today.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
if (diyStore.editComponent.start.defaultControl && starTime == '' && diyStore.editComponent.end.dateWay == 'diy') {
|
||||
res.code = false
|
||||
res.message = t('startDataTips')
|
||||
return res
|
||||
}
|
||||
if (diyStore.editComponent.end.defaultControl && endTime == '' && diyStore.editComponent.end.dateWay == 'diy') {
|
||||
res.code = false
|
||||
res.message = t('endDataTips')
|
||||
return res
|
||||
}
|
||||
|
||||
if (diyStore.editComponent.start.defaultControl && diyStore.editComponent.end.defaultControl && timeTurnTimeStamp(starTime) > timeTurnTimeStamp(endTime)) {
|
||||
res.code = false
|
||||
res.message = t('startEndDataTips')
|
||||
return res
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const dateFormat: any = reactive({
|
||||
format1: '',
|
||||
format2: '',
|
||||
format3: ''
|
||||
})
|
||||
|
||||
// 结束日期-禁止的日期
|
||||
const disabledEndDate = (time:Date)=>{
|
||||
let cutoffDate = null
|
||||
let bool = false
|
||||
if (diyStore.editComponent.start && diyStore.editComponent.start.defaultControl) {
|
||||
if (diyStore.editComponent.start.dateWay == 'diy') {
|
||||
cutoffDate = new Date(diyStore.editComponent.field.default.start.date)
|
||||
} else {
|
||||
cutoffDate = new Date()
|
||||
}
|
||||
bool = time.getTime() < cutoffDate.getTime()
|
||||
}
|
||||
return bool
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
let today = new Date()
|
||||
let endDate = new Date()
|
||||
endDate.setDate(endDate.getDate() + 7) // 设置日期为7天后的日期
|
||||
|
||||
if (diyStore.editComponent.field.default.start.timestamp) {
|
||||
diyStore.editComponent.field.default.start.date = today.toISOString().split('T')[0]
|
||||
diyStore.editComponent.field.default.start.timestamp = parseInt(today.getTime() / 1000)
|
||||
}
|
||||
|
||||
if (diyStore.editComponent.field.default.end.timestamp) {
|
||||
diyStore.editComponent.field.default.end.date = endDate.toISOString().split('T')[0]
|
||||
diyStore.editComponent.field.default.end.timestamp = parseInt(endDate.getTime() / 1000)
|
||||
}
|
||||
|
||||
let year = today.getFullYear()
|
||||
let month = String(today.getMonth() + 1).padStart(2, '0')
|
||||
let day = String(today.getDate()).padStart(2, '0')
|
||||
|
||||
const hours = String(today.getHours()).padStart(2, '0')
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0')
|
||||
dateFormat.format1 = `${year}年${month}月${day}日`
|
||||
dateFormat.format2 = `${year}-${month}-${day}`
|
||||
dateFormat.format3 = `${year}/${month}/${day}`
|
||||
dateFormat.format4 = `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
})
|
||||
|
||||
// 开始日期选择器
|
||||
const startDateChange = (date) => {
|
||||
diyStore.editComponent.field.default.start.date = date
|
||||
diyStore.editComponent.field.default.start.timestamp = timeTurnTimeStamp(date)
|
||||
|
||||
let endDate = new Date(date)
|
||||
endDate.setDate(endDate.getDate() + 7)
|
||||
diyStore.editComponent.field.default.end.date = endDate.toISOString().split('T')[0]
|
||||
diyStore.editComponent.field.default.end.timestamp = parseInt(endDate.getTime() / 1000)
|
||||
}
|
||||
// 结束日期选择器
|
||||
const endDateChange = (date) => {
|
||||
diyStore.editComponent.field.default.end.date = date
|
||||
diyStore.editComponent.field.default.end.timestamp = timeTurnTimeStamp(date)
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
101
admin/src/app/views/diy_form/components/edit-form-date.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('dataFormat')">
|
||||
<el-radio-group v-model="diyStore.editComponent.dateFormat" class="!block">
|
||||
<el-radio class="!block" label="YYYY年M月D日">{{ dateFormat.format1 }}</el-radio>
|
||||
<el-radio class="!block" label="YYYY-MM-DD">{{ dateFormat.format2 }}</el-radio>
|
||||
<el-radio class="!block" label="YYYY/MM/DD">{{ dateFormat.format3 }}</el-radio>
|
||||
<el-radio class="!block" label="YYYY-MM-DD HH:mm">{{ dateFormat.format4 }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('defaultValue')">
|
||||
<el-switch v-model="diyStore.editComponent.defaultControl"/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.defaultControl">
|
||||
<el-radio-group v-model="diyStore.editComponent.dateWay">
|
||||
<el-radio label="current">{{ t('currentDate') }}</el-radio>
|
||||
<el-radio label="diy">{{ t('diyDate') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.defaultControl && diyStore.editComponent.dateWay == 'diy'">
|
||||
<el-date-picker v-if="diyStore.editComponent.dateFormat != 'YYYY-MM-DD HH:mm'" v-model="diyStore.editComponent.field.default.date" format="YYYY/MM/DD" value-format="YYYY-MM-DD" type="date" :placeholder="t('dataPlaceholder')" @change="dateChange"/>
|
||||
<el-date-picker v-else v-model="diyStore.editComponent.field.default.date" format="YYYY/MM/DD HH:mm" value-format="YYYY-MM-DD HH:mm" type="datetime" :placeholder="t('dataPlaceholder')" @change="dateChange"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref,reactive,watch,onMounted } from 'vue'
|
||||
import { timeTurnTimeStamp } from '@/utils/common'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
return res
|
||||
}
|
||||
|
||||
const dateFormat: any = reactive({
|
||||
format1: '',
|
||||
format2: '',
|
||||
format3: '',
|
||||
format4: ''
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 初始赋值当天日期
|
||||
const today = new Date();
|
||||
if (!diyStore.editComponent.field.default.date) {
|
||||
diyStore.editComponent.field.default.date = today.toISOString().split('T')[0];
|
||||
diyStore.editComponent.field.default.timestamp = today.getTime() / 1000;
|
||||
}
|
||||
let year = today.getFullYear();
|
||||
let month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
let day = String(today.getDate()).padStart(2, '0');
|
||||
|
||||
const hours = String(today.getHours()).padStart(2, '0');
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0');
|
||||
dateFormat.format1 = `${year}年${month}月${day}日`;
|
||||
dateFormat.format2 = `${year}-${month}-${day}`;
|
||||
dateFormat.format3 = `${year}/${month}/${day}`;
|
||||
dateFormat.format4 = `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
});
|
||||
|
||||
const dateChange = (date: any)=>{
|
||||
diyStore.editComponent.field.default.date = date;
|
||||
diyStore.editComponent.field.default.timestamp = timeTurnTimeStamp(date);
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
48
admin/src/app/views/diy_form/components/edit-form-email.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
43
admin/src/app/views/diy_form/components/edit-form-file.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('限制上传大小')">
|
||||
<el-input v-model.trim="diyStore.editComponent.limitUploadSize" clearable maxlength="15" />
|
||||
/单位MB,目前是Bit,要转换,*1024
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('preventDuplication') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('preventDuplicationTipsOne') }}</p>
|
||||
<p>{{ t('preventDuplicationTipsTwo') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.unique" />
|
||||
</el-form-item>
|
||||
<el-form-item class="display-block">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('privacyProtection') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('privacyProtectionTipsOne') }}</p>
|
||||
<p>{{ t('privacyProtectionTipsTwo') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
|
||||
<div class="text-sm text-gray-400">{{ t('privacyProtectionTipsThree') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
88
admin/src/app/views/diy_form/components/edit-form-image.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('imageLimit')">
|
||||
<el-input v-model.trim="diyStore.editComponent.limit" :placeholder="t('imageLimitPlaceholder')" clearable maxlength="2" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('上传方式')">
|
||||
<el-checkbox-group v-model="diyStore.editComponent.uploadMode" :min="1">
|
||||
<el-checkbox label="拍照上传" value="take_pictures" />
|
||||
<el-checkbox label="从相册选择" value="select_from_album" />
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
if (diyStore.value[index].limit == '') {
|
||||
res.code = false
|
||||
res.message = t('imageLimitPlaceholder')
|
||||
return res
|
||||
}
|
||||
if (isNaN(diyStore.value[index].limit) || !regExp.number.test(diyStore.value[index].limit)) {
|
||||
res.code = false
|
||||
res.message = t('imageLimitErrorTips')
|
||||
return res
|
||||
}
|
||||
if (diyStore.value[index].limit < 0) {
|
||||
res.code = false
|
||||
res.message = t('imageLimitErrorTipsTwo')
|
||||
return res
|
||||
}
|
||||
if (diyStore.value[index].limit == 0) {
|
||||
res.code = false
|
||||
res.message = t('imageLimitErrorTipsThree')
|
||||
return res
|
||||
}
|
||||
if (diyStore.value[index].limit > 9) {
|
||||
res.code = false
|
||||
res.message = t('imafeLimitErrorTipsFour')
|
||||
return res
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 正则表达式
|
||||
const regExp: any = {
|
||||
required: /[\S]+/,
|
||||
number: /^\d{0,10}$/,
|
||||
digit: /^\d{0,10}(.?\d{0,2})$/,
|
||||
special: /^\d{0,10}(.?\d{0,3})$/
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
99
admin/src/app/views/diy_form/components/edit-form-input.vue
Normal file
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('defaultValue') }}</span>
|
||||
<el-tooltip effect="light" :content="t('defaultValueTips')" placement="top">
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model.trim="diyStore.editComponent.field.default" :placeholder="t('defaultValuePlaceholder')" clearable maxlength="18" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('preventDuplication') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('preventDuplicationTipsOne') }}</p>
|
||||
<p>{{ t('preventDuplicationTipsTwo') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.unique" />
|
||||
</el-form-item>
|
||||
<!-- <el-form-item class="display-block">-->
|
||||
<!-- <template #label>-->
|
||||
<!-- <div class="flex items-center">-->
|
||||
<!-- <span class="mr-[3px]">{{ t('隐私保护') }}</span>-->
|
||||
<!-- <el-tooltip effect="light" placement="top">-->
|
||||
<!-- <template #content>-->
|
||||
<!-- <p>会自动将提交的个人信息做加密展示。</p>-->
|
||||
<!-- <p>适用于公开展示收集的数据且不暴露用户隐私。</p>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <QuestionFilled color="#999999" />-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-switch v-model="diyStore.editComponent.field.privacyProtection" />-->
|
||||
<!-- <div class="text-sm text-gray-400">{{ t('提交后自动隐藏文本,仅管理员可查看') }}</div>-->
|
||||
<!-- </el-form-item>-->
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('access')">
|
||||
<el-radio-group v-model="diyStore.editComponent.mode">
|
||||
<el-radio class="!mr-[20px]" label="authorized_wechat_location">{{ t('authorizeWeChatLocation') }}</el-radio>
|
||||
<el-radio label="open_choose_location">{{ t('manuallySelectPositioning') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item class="display-block">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('privacyProtection') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('privacyProtectionTipsOne') }}</p>
|
||||
<p>{{ t('privacyProtectionTipsTwo') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
|
||||
<div class="text-sm text-gray-400">{{ t('privacyProtectionTipsFour') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
85
admin/src/app/views/diy_form/components/edit-form-mobile.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('preventDuplication') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('preventDuplicationTipsOne') }}</p>
|
||||
<p>{{ t('preventDuplicationTipsTwo') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.unique" />
|
||||
</el-form-item>
|
||||
<el-form-item class="display-block">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('privacyProtection') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('privacyProtectionTipsOne') }}</p>
|
||||
<p>{{ t('privacyProtectionTipsTwo') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
|
||||
<div class="text-sm text-gray-400">{{ t('privacyProtectionTipsFive') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
119
admin/src/app/views/diy_form/components/edit-form-number.vue
Normal file
@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('unit')">
|
||||
<el-input v-model.trim="diyStore.editComponent.unit" :placeholder="t('unitPlaceholder')" clearable maxlength="5" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('defaultValue') }}</span>
|
||||
<el-tooltip effect="light" :content="t('defaultValueTips')" placement="top">
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model.trim="diyStore.editComponent.field.default" :placeholder="t('defaultValuePlaceholder')" @keyup="filterDigit($event)" clearable maxlength="18" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<!-- <el-form-item>-->
|
||||
<!-- <template #label>-->
|
||||
<!-- <div class="flex items-center">-->
|
||||
<!-- <span class="mr-[3px]">{{ t('内容防重复') }}</span>-->
|
||||
<!-- <el-tooltip effect="light" placement="top">-->
|
||||
<!-- <template #content>-->
|
||||
<!-- <p>该组件填写的内容不能与已提交的数据重复。</p>-->
|
||||
<!-- <p>极端情况下可能存在延时导致限制失效。</p>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <QuestionFilled color="#999999" />-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-switch v-model="diyStore.editComponent.field.unique" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item class="display-block">-->
|
||||
<!-- <template #label>-->
|
||||
<!-- <div class="flex items-center">-->
|
||||
<!-- <span class="mr-[3px]">{{ t('隐私保护') }}</span>-->
|
||||
<!-- <el-tooltip effect="light" placement="top">-->
|
||||
<!-- <template #content>-->
|
||||
<!-- <p>会自动将提交的个人信息做加密展示。</p>-->
|
||||
<!-- <p>适用于公开展示收集的数据且不暴露用户隐私。</p>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <QuestionFilled color="#999999" />-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-switch v-model="diyStore.editComponent.field.privacyProtection" />-->
|
||||
<!-- <div class="text-sm text-gray-400">{{ t('提交后自动隐藏数字,仅管理员可查看') }}</div>-->
|
||||
<!-- </el-form-item>-->
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { filterDigit } from '@/utils/common'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
if(diyStore.value[index].field.default){
|
||||
if (isNaN(diyStore.value[index].field.default) || !regExp.digit.test(diyStore.value[index].field.default)) {
|
||||
res.code = false;
|
||||
res.message = t('defaultErrorTips');
|
||||
} else if (diyStore.value[index].field.default < 0) {
|
||||
res.code = false;
|
||||
res.message = t('defaultMustZeroTips')
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 正则表达式
|
||||
const regExp: any = {
|
||||
required: /[\S]+/,
|
||||
number: /^\d{0,10}$/,
|
||||
digit: /^\d{0,10}(.?\d{0,2})$/,
|
||||
special: /^\d{0,10}(.?\d{0,3})$/
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
214
admin/src/app/views/diy_form/components/edit-form-radio.vue
Normal file
@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('style')">
|
||||
<el-radio-group v-model="diyStore.editComponent.style">
|
||||
<el-radio label="style-1">{{ t('defaultSources') }}</el-radio>
|
||||
<el-radio label="style-2">{{ t('listStyle') }}</el-radio>
|
||||
<el-radio label="style-3">{{ t('dropDownStyle') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('option')">
|
||||
<div ref="formRadioRef">
|
||||
<div v-for="(option, index) in diyStore.editComponent.options" :key="option.id" class="option-item flex items-center mb-[15px]">
|
||||
<el-input v-model="diyStore.editComponent.options[index].text" class="!w-[215px]" :placeholder="t('optionPlaceholder')" clearable maxlength="30" />
|
||||
<span v-if="diyStore.editComponent.options.length > 1" @click="removeOption(index)" class="cursor-pointer ml-[5px] nc-iconfont nc-icon-shanchu-yuangaizhiV6xx"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-primary cursor-pointer mr-[10px]" @click="addOption">{{ t('addSingleOption') }}</span>
|
||||
<el-popover :visible="visible" placement="bottom" :width="300">
|
||||
<p class="mb-[5px]">{{ t('addMultipleOption') }}</p>
|
||||
<p class="text-[#888] text-[12px] mb-[5px]">{{ t('addOptionTips') }}</p>
|
||||
<el-input v-model.trim="optionsValue" type="textarea" clearable maxlength="200" show-word-limit />
|
||||
<div class="mt-[10px] text-right">
|
||||
<el-button size="small" text @click="visible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button size="small" type="primary" @click="batchAddOptions">{{ t('confirm') }}</el-button>
|
||||
</div>
|
||||
<template #reference>
|
||||
<span class="text-primary cursor-pointer" @click="visible = true">{{ t('addMultipleOption') }}</span>
|
||||
</template>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item class="display-block">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('逻辑规则') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>支持选择某个选项后,显示特定的组件。</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<el-button plain>{{ t('添加字段显示规则') }}</el-button>
|
||||
<span class="mr-[3px]">1条字段显示规则</span>
|
||||
<span class="text-primary cursor-pointer" @click="">设置</span>
|
||||
</div>
|
||||
</el-form-item> -->
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<!-- <el-form label-width="100px" class="px-[10px]">-->
|
||||
<!-- <el-form-item class="display-block">-->
|
||||
<!-- <template #label>-->
|
||||
<!-- <div class="flex items-center">-->
|
||||
<!-- <span class="mr-[3px]">{{ t('隐私保护') }}</span>-->
|
||||
<!-- <el-tooltip effect="light" placement="top">-->
|
||||
<!-- <template #content>-->
|
||||
<!-- <p>会自动将提交的个人信息做加密展示。</p>-->
|
||||
<!-- <p>适用于公开展示收集的数据且不暴露用户隐私。</p>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <QuestionFilled color="#999999" />-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-switch v-model="diyStore.editComponent.field.privacyProtection" />-->
|
||||
<!-- <div class="text-sm text-gray-400">{{ t('提交后自动隐藏内容,仅管理员可查看') }}</div>-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<!-- </el-form>-->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, onMounted, nextTick} from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import Sortable from 'sortablejs'
|
||||
import { range } from 'lodash-es'
|
||||
import { FormInstance, ElMessage } from "element-plus";
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
let pass = true;
|
||||
for (let i = 0; i < diyStore.value[index].options.length; i++) {
|
||||
if (!diyStore.value[index].options[i].text) {
|
||||
res.code = false;
|
||||
res.message = t('optionPlaceholder');
|
||||
pass = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pass) return res;
|
||||
|
||||
let uniqueOptions = uniqueByKey(diyStore.value[index].options, 'text')
|
||||
|
||||
if (uniqueOptions.length != diyStore.value[index].options.length) {
|
||||
res.code = false;
|
||||
res.message = t('errorTipsOne')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
diyStore.editComponent.options.forEach((item: any) => {
|
||||
if (!item.id) item.id = diyStore.generateRandom()
|
||||
})
|
||||
|
||||
const visible = ref(false)
|
||||
const optionsValue = ref()
|
||||
const addOption = () => {
|
||||
diyStore.editComponent.options.push({
|
||||
id: diyStore.generateRandom(),
|
||||
text: '选项' + (diyStore.editComponent.options.length + 1)
|
||||
});
|
||||
};
|
||||
|
||||
const removeOption = (index:any) => {
|
||||
diyStore.editComponent.options.splice(index, 1);
|
||||
}
|
||||
|
||||
const batchAddOptions = () => {
|
||||
if (optionsValue.value.trim()) {
|
||||
const newOptions = optionsValue.value.split(',').map((option: any) => {
|
||||
return {
|
||||
id: diyStore.generateRandom(),
|
||||
text: option.trim()
|
||||
};
|
||||
}).filter((option: any) => option.text !== '');
|
||||
|
||||
// 去除重复的选项
|
||||
const uniqueNewOptions = uniqueByKey(newOptions, 'text');
|
||||
|
||||
// 过滤掉已存在的选项
|
||||
const filteredNewOptions = uniqueNewOptions.filter(newOption =>
|
||||
!diyStore.editComponent.options.some(existingOption => existingOption.text === newOption.text)
|
||||
);
|
||||
|
||||
// 如果有新的选项,添加到选项列表中
|
||||
if (filteredNewOptions.length > 0) {
|
||||
diyStore.editComponent.options.push(...filteredNewOptions);
|
||||
} else {
|
||||
ElMessage({
|
||||
message: t('errorTipsTwo'),
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
optionsValue.value = '';
|
||||
visible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 数组去重
|
||||
const uniqueByKey = (arr: any, key: any) => {
|
||||
const seen = new Set();
|
||||
return arr.filter((item: any) => {
|
||||
const serializedKey = JSON.stringify(item[key]);
|
||||
return seen.has(serializedKey) ? false : seen.add(serializedKey);
|
||||
});
|
||||
}
|
||||
|
||||
const formRadioRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const sortable = Sortable.create(formRadioRef.value, {
|
||||
group: 'option-item',
|
||||
animation: 200,
|
||||
onEnd: event => {
|
||||
const temp = diyStore.editComponent.options[event.oldIndex!]
|
||||
diyStore.editComponent.options.splice(event.oldIndex!, 1)
|
||||
diyStore.editComponent.options.splice(event.newIndex!, 0, temp)
|
||||
sortable.sort(
|
||||
range(diyStore.editComponent.options.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
113
admin/src/app/views/diy_form/components/edit-form-submit.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('floatBtnButton')" class="display-block">
|
||||
<el-radio-group v-model="diyStore.editComponent.btnPosition" @change="btnPositionChangeFn">
|
||||
<el-radio label="follow_content">{{ t('followContent') }}</el-radio>
|
||||
<el-radio label="hover_screen_bottom">{{ t('hoverScreenBottom') }}</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="text-sm text-gray-400 mb-[5px] leading-[1.4]" v-show="diyStore.editComponent.btnPosition == 'follow_content'">{{ t('btnTips') }}</div>
|
||||
<div class="text-sm text-gray-400 mb-[5px]" v-show="diyStore.editComponent.btnPosition == 'hover_screen_bottom'">{{ t('btnTipsTwo') }}</div>
|
||||
<div class="text-sm text-gray-400 mb-[10px] leading-[1.4]">{{ t('btnTipsThree') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('submitBtn') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('submitBtnName')">
|
||||
<el-input v-model.trim="diyStore.editComponent.submitBtn.text" :placeholder="t('btnNamePlaceholder')" clearable maxlength="10" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.submitBtn.color" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('subTextBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.submitBtn.bgColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('resetBtn') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('carouselSearchTabControl')">
|
||||
<el-switch v-model="diyStore.editComponent.resetBtn.control" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('submitBtnName')">
|
||||
<el-input v-model.trim="diyStore.editComponent.resetBtn.text" :placeholder="t('btnNamePlaceholder')" clearable maxlength="10" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.resetBtn.color" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('subTextBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.resetBtn.bgColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('btnStyle') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('topRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.topElementRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('bottomRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.bottomElementRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 单选
|
||||
const btnPositionChangeFn = (e)=>{
|
||||
if(e == 'hover_screen_bottom'){
|
||||
diyStore.editComponent.margin.bottom = 0;
|
||||
diyStore.editComponent.margin.both = 0;
|
||||
diyStore.editComponent.margin.top = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
if (diyStore.value[index].submitBtn.text == '') {
|
||||
res.code = false;
|
||||
res.message = t('submitBtnNamePlaceholder');
|
||||
return res;
|
||||
}
|
||||
if (diyStore.value[index].resetBtn.text == '') {
|
||||
res.code = false;
|
||||
res.message = t('resetBtnNamePlaceholder');
|
||||
return res;
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
44
admin/src/app/views/diy_form/components/edit-form-table.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
todo 此处编写表格组件的属性
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('defaultValue') }}</span>
|
||||
<el-tooltip effect="light" :content="t('defaultValueTips')" placement="top">
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model.trim="diyStore.editComponent.field.default" :placeholder="t('defaultValuePlaceholder')" clearable maxlength="18" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('rowCount')">
|
||||
<el-input v-model.trim="diyStore.editComponent.rowCount" :placeholder="t('rowCountPlaceholder')" clearable maxlength="2" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<!--<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item class="display-block">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('隐私保护') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>会自动将提交的个人信息做加密展示。</p>
|
||||
<p>适用于公开展示收集的数据且不暴露用户隐私。</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
|
||||
<div class="text-sm text-gray-400">{{ t('提交后自动隐藏文本,仅管理员可查看') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>-->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
201
admin/src/app/views/diy_form/components/edit-form-time-scope.vue
Normal file
@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('startTime') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.start.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('defaultValue')">
|
||||
<el-switch v-model="diyStore.editComponent.start.defaultControl"/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.start.defaultControl">
|
||||
<el-radio-group v-model="diyStore.editComponent.start.timeWay">
|
||||
<el-radio label="current">{{ t('currentTime') }}</el-radio>
|
||||
<el-radio label="diy">{{ t('diyTime') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.start.defaultControl && diyStore.editComponent.start.timeWay == 'diy'">
|
||||
<el-time-picker v-model="diyStore.editComponent.field.default.start.date" :placeholder="t('startTimePlaceholder')" format="HH:mm" value-format="HH:mm" @change="startTimePickerChange" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('endTime') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.end.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('defaultValue')">
|
||||
<el-switch v-model="diyStore.editComponent.end.defaultControl" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.end.defaultControl">
|
||||
<el-radio-group v-model="diyStore.editComponent.end.timeWay">
|
||||
<el-radio label="current">{{ t('currentTime') }}</el-radio>
|
||||
<el-radio label="diy">{{ t('diyTime') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.end.defaultControl && diyStore.editComponent.end.timeWay == 'diy'">
|
||||
<el-time-picker :disabled-hours="disabledHours" :disabled-minutes="disabledMinutes" v-model="diyStore.editComponent.field.default.end.date" :placeholder="t('endTimePlaceholder')" format="HH:mm" value-format="HH:mm" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('textStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('textFontSize')">
|
||||
<el-slider v-model="diyStore.editComponent.fontSize" show-input size="small" class="ml-[10px] diy-nav-slider" :min="12" :max="18" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textFontWeight')">
|
||||
<el-radio-group v-model="diyStore.editComponent.fontWeight">
|
||||
<el-radio :label="'normal'">{{ t('fontWeightNormal') }}</el-radio>
|
||||
<el-radio :label="'bold'">{{ t('fontWeightBold') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.textColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref,onMounted } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
let starTime = diyStore.value[index].field.default.start.date;
|
||||
let endTime = diyStore.value[index].field.default.end.date;
|
||||
|
||||
const today = new Date();
|
||||
const hours = String(today.getHours()).padStart(2, '0');
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0');
|
||||
|
||||
if(diyStore.editComponent.start.timeWay == 'current'){
|
||||
starTime = `${hours}:${minutes}`;
|
||||
}
|
||||
if(diyStore.editComponent.end.timeWay == 'current'){
|
||||
endTime = `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
if(diyStore.editComponent.start.defaultControl && starTime == ''){
|
||||
res.code = false
|
||||
res.message = t('startTimeTips')
|
||||
return res
|
||||
}
|
||||
if(diyStore.editComponent.end.defaultControl && endTime == ''){
|
||||
res.code = false
|
||||
res.message = t('endTimeTips')
|
||||
return res
|
||||
}
|
||||
|
||||
if(diyStore.editComponent.start.defaultControl && diyStore.editComponent.end.defaultControl && timeInvertSecond(starTime) > timeInvertSecond(endTime)){
|
||||
res.code = false
|
||||
res.message = t('startEndTimeTips')
|
||||
return res
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
const today = new Date();
|
||||
const hours = String(today.getHours()).padStart(2, '0');
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0');
|
||||
|
||||
if(!diyStore.editComponent.field.default.start.date){
|
||||
diyStore.editComponent.field.default.start.date = `${hours}:${minutes}`;
|
||||
diyStore.editComponent.field.default.start.timestamp = timeInvertSecond(`${hours}:${minutes}`);
|
||||
}
|
||||
if(!diyStore.editComponent.field.default.end.date){
|
||||
let endDate = new Date();
|
||||
endDate.setHours(today.getHours(), today.getMinutes() + 10, 0, 0); // 在当前时间基础上加 10 分钟
|
||||
const endHours = String(endDate.getHours()).padStart(2, '0');
|
||||
const endMinutes = String(endDate.getMinutes()).padStart(2, '0');
|
||||
|
||||
diyStore.editComponent.field.default.end.date = `${endHours}:${endMinutes}`;
|
||||
diyStore.editComponent.field.default.end.timestamp = timeInvertSecond(`${endHours}:${endMinutes}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 开始时间选择器
|
||||
const startTimePickerChange = (e) => {
|
||||
diyStore.editComponent.field.default.start.timestamp = timeInvertSecond(e);
|
||||
|
||||
const startTimeArr = e.split(":");
|
||||
const date = new Date();
|
||||
date.setHours(parseInt(startTimeArr[0]), parseInt(startTimeArr[1]), 0, 0);
|
||||
date.setMinutes(date.getMinutes() + 10);
|
||||
const updatedEndTime = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
diyStore.editComponent.field.default.end.date = updatedEndTime;
|
||||
diyStore.editComponent.field.default.end.timestamp = timeInvertSecond(updatedEndTime);
|
||||
}
|
||||
|
||||
// 结束时间选择器
|
||||
const endTimePickerChange = (e)=>{
|
||||
diyStore.editComponent.field.default.end.timestamp = timeInvertSecond(e);
|
||||
}
|
||||
|
||||
const disabledHours = () => {
|
||||
let timeArr = diyStore.editComponent.field.default.start.date.split(":")
|
||||
return makeRange(0,timeArr[0]);
|
||||
}
|
||||
|
||||
const disabledMinutes = (hour: number) => {
|
||||
let timeArr = diyStore.editComponent.field.default.start.date.split(":")
|
||||
return makeRange(0,timeArr[1]);
|
||||
}
|
||||
|
||||
const makeRange = (start: number, end: number) => {
|
||||
const result: number[] = []
|
||||
for (let i = start; i < end; i++) {
|
||||
result.push(i)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const timeInvertSecond = (time:any)=>{
|
||||
let arr = time.split(":");
|
||||
let num = 0;
|
||||
if(arr[0]){
|
||||
num += arr[0] * 60 * 60;
|
||||
}
|
||||
if(arr[1]){
|
||||
num += arr[1] * 60;
|
||||
}
|
||||
if(arr[2]){
|
||||
num += arr[2];
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
89
admin/src/app/views/diy_form/components/edit-form-time.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('defaultValue')">
|
||||
<el-switch v-model="diyStore.editComponent.defaultControl" @change="changeDateDefaultControl" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.defaultControl">
|
||||
<el-radio-group v-model="diyStore.editComponent.timeWay">
|
||||
<el-radio label="current">{{ t('currentTime') }}</el-radio>
|
||||
<el-radio label="diy">{{ t('diyTime') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.defaultControl && diyStore.editComponent.timeWay == 'diy'">
|
||||
<el-time-picker v-model="diyStore.editComponent.field.default" :placeholder="t('timePlaceholder')" format="HH:mm" value-format="HH:mm" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref,watch,onMounted } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
return res
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始赋值当天时间
|
||||
if (!diyStore.editComponent.field.default) {
|
||||
const today = new Date();
|
||||
const hours = String(today.getHours()).padStart(2, '0');
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0');
|
||||
diyStore.editComponent.field.default = `${hours}:${minutes}`;
|
||||
}
|
||||
});
|
||||
|
||||
const changeDateDefaultControl = (val: any) => {
|
||||
if (val) {
|
||||
const today = new Date();
|
||||
const hours = String(today.getHours()).padStart(2, '0');
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0');
|
||||
diyStore.editComponent.field.default = `${hours}:${minutes}`;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => diyStore.editComponent.timeWay,
|
||||
(newVal) => {
|
||||
const today = new Date();
|
||||
const hours = String(today.getHours()).padStart(2, '0');
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0');
|
||||
diyStore.editComponent.field.default = `${hours}:${minutes}`;
|
||||
}
|
||||
);
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
42
admin/src/app/views/diy_form/components/edit-form-video.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('上传方式')">
|
||||
<el-radio-group v-model="diyStore.editComponent.uploadMode">
|
||||
<el-radio label="shoot_and_album">{{ t('拍摄和相册') }}</el-radio>
|
||||
<el-radio label="shoot_only">{{ t('只允许拍摄') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
177
admin/src/app/views/diy_form/components/form-select-content.vue
Normal file
@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form :inline="true" :model="tableData.searchParam" ref="searchFormRef">
|
||||
<el-form-item :label="t('formSelectContentTitle')" prop="title" class="form-item-wrap">
|
||||
<el-input v-model.trim="tableData.searchParam.title" :placeholder="t('formSelectContentTitlePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('formSelectContentTypeName')" prop="type" class="form-item-wrap">
|
||||
<el-select v-model="tableData.searchParam.type" :placeholder="t('formSelectContentTypeNamePlaceholder')">
|
||||
<el-option :label="t('formSelectContentTypeAll')" value="" />
|
||||
<el-option v-for="(item, key) in formType" :label="item.title" :value="key" :key="key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="form-item-wrap">
|
||||
<el-button type="primary" @click="loadList()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table :data="tableData.data" size="large" ref="tableRef" v-loading="tableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !tableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column min-width="7%">
|
||||
<template #default="{ row }">
|
||||
<el-checkbox v-model="row.checked" @change="handleCheckChange($event,row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="page_title" :label="t('formSelectContentTitle')" min-width="65%" />
|
||||
<el-table-column prop="type_name" :label="t('formSelectContentTypeName')" min-width="25%" />
|
||||
</el-table>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="tableData.total"
|
||||
@size-change="loadList()" @current-change="loadList" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, nextTick } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getFormType, getDiyFormSelectPageList } from '@/app/api/diy_form'
|
||||
import { FormInstance, ElMessage } from "element-plus";
|
||||
|
||||
const prop = defineProps({
|
||||
formId: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const formType: any = reactive({}) // 表单类型
|
||||
|
||||
const searchFormRef = ref<FormInstance>()
|
||||
|
||||
const tableRef = ref();
|
||||
|
||||
const tableData: any = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
title: '',
|
||||
type: '',
|
||||
verify_form_ids: []
|
||||
}
|
||||
})
|
||||
|
||||
// 已选万能表单
|
||||
const selectData: any = reactive({
|
||||
form_id: prop.formId
|
||||
})
|
||||
|
||||
// 获取自定义表单列表
|
||||
const loadList = (page: number = 1) => {
|
||||
tableData.loading = true
|
||||
tableData.page = page
|
||||
|
||||
if (selectData.form_id) {
|
||||
tableData.searchParam.verify_form_ids = [selectData.form_id]
|
||||
}
|
||||
|
||||
getDiyFormSelectPageList({
|
||||
page: tableData.page,
|
||||
limit: tableData.limit,
|
||||
...tableData.searchParam
|
||||
}).then(res => {
|
||||
tableData.loading = false
|
||||
tableData.data = res.data.data
|
||||
|
||||
tableData.data.forEach((item: any) => {
|
||||
item.checked = item.form_id == selectData.form_id
|
||||
})
|
||||
tableData.total = res.data.total
|
||||
setGoodsSelected()
|
||||
}).catch(() => {
|
||||
tableData.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
// 获取万能表单类型
|
||||
const loadFormType = (addon = '') => {
|
||||
getFormType({}).then(res => {
|
||||
for (let key in formType) {
|
||||
delete formType[key];
|
||||
}
|
||||
|
||||
for (const key in res.data) {
|
||||
formType[key] = res.data[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loadFormType();
|
||||
loadList()
|
||||
|
||||
const handleCheckChange = (isSelect: any, row: any) => {
|
||||
if (isSelect) {
|
||||
selectData.form_id = row.form_id
|
||||
} else {
|
||||
selectData.form_id = 0 // 未选中,移除当前
|
||||
}
|
||||
setGoodsSelected()
|
||||
}
|
||||
|
||||
// 表格设置选中状态
|
||||
const setGoodsSelected = () => {
|
||||
nextTick(() => {
|
||||
for (let i = 0; i < tableData.data.length; i++) {
|
||||
tableData.data[i].checked = false
|
||||
if (selectData.form_id == tableData.data[i].form_id) {
|
||||
tableData.data[i].checked = true
|
||||
Object.assign(selectData, tableData.data[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
loadList()
|
||||
}
|
||||
|
||||
const getData = () => {
|
||||
if (selectData.form_id == 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: `${ t('formSelectContentTips') }`
|
||||
})
|
||||
return;
|
||||
}
|
||||
return {
|
||||
name: 'DIY_FORM',
|
||||
title: selectData.page_title,
|
||||
url: `/app/pages/index/diy_form?form_id=${ selectData.form_id }`,
|
||||
action: '',
|
||||
formId: selectData.form_id
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-item-wrap {
|
||||
margin-right: 10px !important;
|
||||
margin-bottom: 10px !important;
|
||||
|
||||
&.last-child {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
144
admin/src/app/views/diy_form/components/form-spread-popup.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog v-model="showDialog" :title="t('formPromotion')" width="500px" :destroy-on-close="true">
|
||||
|
||||
<el-tabs v-model="channel" class="mb-[10px]">
|
||||
<el-tab-pane label="H5" name="h5"></el-tab-pane>
|
||||
<el-tab-pane label="微信小程序" name="weapp"></el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="promote-flex flex" v-if="channel == 'h5'">
|
||||
<div class="promote-img flex justify-center items-center bg-[#f8f8f8] w-[150px] h-[150px]">
|
||||
<el-image :src="wapImage" />
|
||||
</div>
|
||||
|
||||
<div class="px-[20px] flex-1">
|
||||
<div class="mb-[10px]">{{ t('promoteUrl') }}</div>
|
||||
<el-input class="mb-[10px]" readonly :value="wapPreview">
|
||||
<template #append>
|
||||
<el-button @click="copyEvent(wapPreview)">{{ t('copy') }}</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<a class="text-primary" :href="wapImage" download>{{ t('downLoadQRCode') }}</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="promote-flex flex" v-if="channel == 'weapp'">
|
||||
<div class="promote-img flex justify-center items-center bg-[#f8f8f8] w-[150px] h-[150px]">
|
||||
<el-image :src="img(weappData.path)" v-if="weappData.path" class="w-[150px] h-[150px]">
|
||||
<template #error>
|
||||
<div class="w-[150px] h-[150px] text-[14px] text-center leading-[150px]">{{ t('configureFailed') }}</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="w-[150px] h-[150px] text-[14px] text-center leading-[150px]">{{ t('configureFailed') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="px-[20px] flex-1">
|
||||
<a class="text-primary" :href="img(weappData.path)" target="_blank" download>{{ t('downLoadQRCode') }}</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import QRCode from 'qrcode'
|
||||
import storage from '@/utils/storage'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { getUrl } from '@/app/api/sys'
|
||||
import { getDiyFormQrcode } from '@/app/api/diy_form'
|
||||
import { img } from '@/utils/common'
|
||||
|
||||
const form: any = reactive({})
|
||||
const showDialog = ref(false)
|
||||
const channel = ref('h5')
|
||||
const wapUrl = ref('')
|
||||
const wapDomain = ref('')
|
||||
const wapImage = ref('')
|
||||
const wapPreview = ref('')
|
||||
const page = ref('')
|
||||
const weappData = reactive({
|
||||
path: ''
|
||||
})
|
||||
|
||||
getUrl().then((res: any) => {
|
||||
wapUrl.value = res.data.wap_url
|
||||
|
||||
// 生产模式禁止
|
||||
if (import.meta.env.MODE == 'production') return
|
||||
|
||||
wapDomain.value = res.data.wap_domain
|
||||
|
||||
// env文件配置过wap域名
|
||||
if (wapDomain.value) {
|
||||
wapUrl.value = wapDomain.value + '/wap'
|
||||
}
|
||||
|
||||
const wapDomainStorage = storage.get('wap_domain')
|
||||
if (wapDomainStorage) {
|
||||
wapUrl.value = wapDomainStorage
|
||||
}
|
||||
})
|
||||
|
||||
const loadQrcode = () => {
|
||||
wapPreview.value = `${wapUrl.value}${page.value}`
|
||||
// errorCorrectionLevel:密度容错率L(低)H(高)
|
||||
QRCode.toDataURL(wapPreview.value, { errorCorrectionLevel: 'L', margin: 0, width: 120 }).then(url => {
|
||||
wapImage.value = url
|
||||
})
|
||||
}
|
||||
|
||||
const show = (data: any) => {
|
||||
channel.value = 'h5';
|
||||
Object.assign(form, data)
|
||||
page.value = `/app/pages/index/diy_form?form_id=${form.form_id}`
|
||||
|
||||
loadQrcode()
|
||||
getDiyFormQrcodeFn();
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const getDiyFormQrcodeFn = ()=>{
|
||||
getDiyFormQrcode({
|
||||
form_id:form.form_id
|
||||
}).then((res:any)=>{
|
||||
if(res.data) {
|
||||
weappData.path = res.data.path;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 复制
|
||||
const { copy, isSupported, copied } = useClipboard()
|
||||
const copyEvent = (text: string) => {
|
||||
if (!isSupported.value) {
|
||||
ElMessage({
|
||||
message: t('notSupportCopy'),
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
copy(text)
|
||||
}
|
||||
|
||||
watch(copied, () => {
|
||||
if (copied.value) {
|
||||
ElMessage({
|
||||
message: t('copySuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
show
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
435
admin/src/app/views/diy_form/components/form-submit-popup.vue
Normal file
@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog v-model="showDialog" :title="t('submitSuccess')" width="850px" :close-on-press-escape="true" :destroy-on-close="true" :close-on-click-modal="false">
|
||||
|
||||
<div class="flex flex-1 mt-[24px] mx-[24px] mb-0">
|
||||
<div class="preview-wrap">
|
||||
<div class="absolute z-1 left-0 top-0">
|
||||
<img src="@/app/assets/images/diy_form/mobile_tabbar.png" class="w-[324px]" />
|
||||
</div>
|
||||
<div class="absolute z-1 left-0 bottom-0">
|
||||
<img src="@/app/assets/images/diy_form/mobile_bottom.png" class="w-[324px]" />
|
||||
</div>
|
||||
|
||||
<div class="page-wrap">
|
||||
<div class="px-[13px] flex flex-col items-center flex-1">
|
||||
<div class="flex items-center justify-center w-[48px] h-[48px] text-[40px] p-[4px] mt-[32px] mb-[16px] mx-auto rounded-[50%]">
|
||||
<el-icon><SuccessFilled color="#20bf64" /></el-icon>
|
||||
</div>
|
||||
<div class="record-name">
|
||||
<span class="text-[#1E1E1E] font-bold text-[24px]" v-if="formData.tips_type == 'default'">{{ t('writeSuccess') }}</span>
|
||||
<span class="text-[#1E1E1E] font-bold text-[16px]" v-else-if="formData.tips_type == 'diy'">{{ formData.tips_text ? formData.tips_text : '填写成功' }}</span>
|
||||
</div>
|
||||
<div class="to-detail">
|
||||
<div class="text-[14px] mt-[16px] py-[4px] px[8px] text-[#576b95]">{{ t('viewFillingDetails') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative pt-[8px] pb-[48px] h-[112px]">
|
||||
<div v-if="formData.success_after_action.finish" class="!mt-[16px] rounded-[3px] mx-auto text-[15px] w-[100px] min-w-[160px] h-[32px] leading-[32px] text-center max-w-[274px] truncate bg-[#20bf64] text-[#ffffff]">
|
||||
<div class="text-[15px]">{{ t('finish') }}</div>
|
||||
</div>
|
||||
<div v-if="formData.success_after_action.goback" class="!mt-[16px] rounded-[3px] mx-auto text-[15px] w-[100px] min-w-[160px] h-[32px] leading-[32px] text-center max-w-[274px] truncate bg-[#f2f2f2] text-[#353535]">
|
||||
<div class="text-[14px]">{{ t('back') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核销凭证 todo 后续完善 -->
|
||||
<!-- <div class="page-wrap verify-voucher-wrap" style="display:none;">-->
|
||||
|
||||
<!-- <div class="tips-wrap">感谢你的填写,以下是你的核销凭证</div>-->
|
||||
<!-- <div class="qrcode-wrap">-->
|
||||
<!-- <div class="text-[14px] text-[#333]">请妥善保存你的核销凭证</div>-->
|
||||
<!-- <div class="text-[20px] font-bold text-[#333] my-[10px]">现场出示凭证</div>-->
|
||||
<!-- <el-image class="w-[180px]" :src="wapImage" />-->
|
||||
<!-- <div class="text-primary mt-[10px]">保存凭证</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="relative pt-[8px] pb-[48px] h-[112px]">-->
|
||||
<!-- <div class="!mt-[16px] rounded-[3px] mx-auto text-[15px] w-[100px] min-w-[160px] h-[32px] leading-[32px] text-center max-w-[274px] truncate bg-[#20bf64] text-[#ffffff]">-->
|
||||
<!-- <div class="text-[15px]">返回二维码</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="!mt-[16px] rounded-[3px] mx-auto text-[15px] w-[100px] min-w-[160px] h-[32px] leading-[32px] text-center max-w-[274px] truncate bg-[#fff] text-[#353535]">-->
|
||||
<!-- <div class="text-[14px]">完成</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="text-[14px] mt-[16px] py-[4px] px[8px] text-[#576b95]">查看填写详情</div>-->
|
||||
|
||||
<!-- </div>-->
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class="item-wrap">
|
||||
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">{{ t('afterSubmission') }}</div>
|
||||
<el-radio-group v-model="formData.submit_after_action" class="!block">
|
||||
<el-radio label="text" class="!flex">
|
||||
<span class="mr-[3px]">{{ t('displayTextMessages') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('displayTextMessagesTips') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</el-radio>
|
||||
<!-- todo 后续完善 -->
|
||||
<!-- <el-radio label="voucher" class="!flex">-->
|
||||
<!-- <span class="mr-[3px]">{{ t('获取核销凭证') }}</span>-->
|
||||
<!-- <el-tooltip effect="light" placement="top">-->
|
||||
<!-- <template #content>-->
|
||||
<!-- <p>{{ t('提交后页面会将提交的表单记录内容生成二维码并展示,可选择设置两种不同的二维码内容。适合核销、数据录入等场景。') }}</p>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <QuestionFilled color="#999999" />-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </el-radio>-->
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="item-wrap" v-if="formData.submit_after_action == 'text'">
|
||||
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">{{ t('promptText') }}</div>
|
||||
<div>
|
||||
<el-radio-group v-model="formData.tips_type" class="!block">
|
||||
<el-radio label="default" class="!block">
|
||||
<span class="mr-[3px]">{{ t('defaultPrompt') }}</span>
|
||||
<span class="!text-[#999] text-[12px] ml-[8px]">{{ t('defaultPromptTips') }}</span>
|
||||
</el-radio>
|
||||
<el-radio label="diy" class="!block">{{ t('diyPrompt') }}</el-radio>
|
||||
</el-radio-group>
|
||||
<el-input v-if="formData.tips_type == 'diy'" v-model.trim="formData.tips_text" :placeholder="t('tipsTextPlaceholder')" class="w-[350px]" maxlength="30" clearable show-word-limit />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核销凭证 todo 后续完善 -->
|
||||
<template v-else-if="formData.submit_after_action == 'voucher'">
|
||||
|
||||
<div class="item-wrap">
|
||||
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">{{ t('validityPeriodOfVoucher') }}</div>
|
||||
<div>
|
||||
<el-radio-group v-model="formData.time_limit_type" class="!block">
|
||||
<el-radio label="no_limit" class="!block">{{ t('noLimit') }}</el-radio>
|
||||
<el-radio label="specify_time" class="!block">
|
||||
<span class="mr-[3px]">{{ t('specifyTime') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('specifyTimeTips') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</el-radio>
|
||||
<el-radio label="submission_time" class="!block">
|
||||
<span class="mr-[3px]">{{ t('submissionTime') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p class="w-[250px]">{{ t('submissionTimeTips') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<el-date-picker v-if="formData.time_limit_type == 'specify_time'" v-model="formData.validity_time" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
|
||||
<div class="flex items-center mt-[5px]" v-if="formData.time_limit_type == 'submission_time'">
|
||||
<span>{{ t('afterSubmissionRecords') }}</span>
|
||||
<!-- <div class="flex items-center px-[5px]">-->
|
||||
<!-- v-model.trim="formData.length"-->
|
||||
<el-input v-model.trim="formData.submission_time_value" @keyup="filterNumber($event)" size="small" clearable class="!w-[100px] px-[5px]" maxlength="3" />
|
||||
<el-select v-model="formData.timeUnit" clearable class="!w-[100px] pr-[5px]" size="small">
|
||||
<el-option v-for="(item, index) in validityOptions" :key="item.value" :label="item.text" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
<span>{{ t('effective') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-wrap">
|
||||
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">{{ t('voucherStyle') }}</div>
|
||||
<div>
|
||||
<el-form-item :label="t('titleAboveTheCode')">
|
||||
<!-- v-model.trim="formData.active_name"-->
|
||||
<el-input clearable :placeholder="t('titleAboveTheCodePlaceholder')" class="input-width" :maxlength="20" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('contentAboveTheCode')">
|
||||
<el-input clearable :placeholder="t('contentAboveTheCodePlaceholder')" class="input-width" :maxlength="20" />
|
||||
<div>
|
||||
<span class="text-primary cursor-pointer mr-[10px]">{{ t('addLinefeeds') }}</span>
|
||||
<span class="text-primary cursor-pointer">{{ t('addFields') }}</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('contentBelowTheCode')">
|
||||
<div class="block">
|
||||
<el-checkbox class="!block" :label="t('submissionRecordTime')" value="" />
|
||||
|
||||
<el-checkbox class="!block !h-[20px]" :label="t('currentTime')" value="" />
|
||||
<div class="text-[#999] ml-[22px]">{{ t('currentTimeTips') }}</div>
|
||||
|
||||
<el-checkbox class="!block" :label="t('dispalyPromptText')" value="" />
|
||||
<el-input class="ml-[22px]" :rows="4" type="textarea" :placeholder="t('tipsTextPlaceholder')" maxlength="100" />
|
||||
|
||||
<el-checkbox class="!block" :label="t('voucherDeadline')" value="" />
|
||||
<el-checkbox class="!block" :label="t('saveVoucher')" value="" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- todo 后续完善 -->
|
||||
<div class="item-wrap">
|
||||
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">
|
||||
<span>{{ t('subsequentPperationButtons') }}</span>
|
||||
<!-- <p class="text-[12px] text-[#999] mt-[4px] font-normal">最多选择2个</p>-->
|
||||
</div>
|
||||
<div class="content-list-wrap">
|
||||
<!-- <el-checkbox-group :min="1" :max="2">-->
|
||||
<!-- <el-checkbox v-model="formData.success_after_action.share" label="转发填写内容" value="share">-->
|
||||
<!-- <div class="text-[#333]">转发填写内容</div>-->
|
||||
<!-- </el-checkbox>-->
|
||||
<!-- <p class="text-[#999] text-[12px] pl-[24px] mt-[4px]">提交表单后,可转发给微信好友查看。支持按钮文案自定义,提醒填表人转发给特定人员查看</p>-->
|
||||
|
||||
<el-checkbox v-model="formData.success_after_action.finish" :label="t('finish')" value="finish">
|
||||
<div class="text-[#333]">{{ t('finish') }}</div>
|
||||
</el-checkbox>
|
||||
<p class="text-[#999] text-[12px] pl-[24px] mt-[4px]">{{ t('finishTips') }}</p>
|
||||
|
||||
<el-checkbox v-model="formData.success_after_action.goback" :label="t('back')" value="goback">
|
||||
<div class="text-[#333]">{{ t('back') }}</div>
|
||||
</el-checkbox>
|
||||
<p class="text-[#999] text-[12px] pl-[24px] mt-[4px]">{{ t('backTips') }}</p>
|
||||
<!-- </el-checkbox-group>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="confirm">{{ t('save') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import QRCode from 'qrcode'
|
||||
import storage from '@/utils/storage'
|
||||
import { filterNumber } from '@/utils/common'
|
||||
import { getUrl } from '@/app/api/sys'
|
||||
import { getFormSubmitConfig,editDiyFormSubmitConfig } from '@/app/api/diy_form'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const repeat = ref(false)
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: 0,
|
||||
form_id: 0,
|
||||
submit_after_action: 'text', // 填表人提交后操作,text:文字信息,voucher:核销凭证
|
||||
tips_type: 'default', // 提示内容类型,default:默认提示,diy:自定义提示
|
||||
tips_text: '', // 自定义提示内容
|
||||
time_limit_type: 'no_limit', // 核销凭证有效期限制类型,no_limit:不限制,specify_time:指定固定开始结束时间,submission_time:按提交时间设置有效期
|
||||
// 核销凭证时间限制规则,json格式 todo 结构待定,后续完善
|
||||
time_limit_rule: {
|
||||
validity_time: [], // 指定固定开始结束时间
|
||||
submission_time_value: '', // 按提交时间设置有效期
|
||||
timeUnit: 'day', // 提交时间单位
|
||||
},
|
||||
// 核销凭证内容,json格式 todo 结构待定,后续完善
|
||||
voucher_content_rule: {},
|
||||
// 填写成功后续操作
|
||||
success_after_action: {
|
||||
share: false, // 转发填写内容
|
||||
finish: true, // 完成
|
||||
goback: true, // 返回
|
||||
}
|
||||
}
|
||||
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const wapUrl = ref('')
|
||||
const wapDomain = ref('')
|
||||
const wapImage = ref('')
|
||||
const wapPreview = ref('')
|
||||
const page = ref('')
|
||||
|
||||
// 核销凭证有效期
|
||||
const validityOptions = reactive([
|
||||
{
|
||||
text:'天',
|
||||
value:'day'
|
||||
},
|
||||
{
|
||||
text:'周',
|
||||
value:'week'
|
||||
},
|
||||
{
|
||||
text:'月',
|
||||
value:'month'
|
||||
},
|
||||
{
|
||||
text:'年',
|
||||
value:'year'
|
||||
},
|
||||
{
|
||||
text:'分钟',
|
||||
value:'minutes'
|
||||
}
|
||||
])
|
||||
|
||||
// getUrl().then((res: any) => {
|
||||
// wapUrl.value = res.data.wap_url
|
||||
//
|
||||
// // 生产模式禁止
|
||||
// if (import.meta.env.MODE == 'production') return
|
||||
//
|
||||
// wapDomain.value = res.data.wap_domain
|
||||
//
|
||||
// // env文件配置过wap域名
|
||||
// if (wapDomain.value) {
|
||||
// wapUrl.value = wapDomain.value + '/wap'
|
||||
// }
|
||||
//
|
||||
// const wapDomainStorage = storage.get('wap_domain')
|
||||
// if (wapDomainStorage) {
|
||||
// wapUrl.value = wapDomainStorage
|
||||
// }
|
||||
// })
|
||||
|
||||
const loadQrcode = () => {
|
||||
wapPreview.value = `${wapUrl.value}${page.value}`
|
||||
// errorCorrectionLevel:密度容错率L(低)H(高)
|
||||
QRCode.toDataURL(wapPreview.value, { errorCorrectionLevel: 'L', margin: 0, width: 120 }).then(url => {
|
||||
wapImage.value = url
|
||||
})
|
||||
}
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
Object.assign(formData, initialFormData)
|
||||
if (row) {
|
||||
const data = await (await getFormSubmitConfig(row.form_id)).data
|
||||
if (data) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
}
|
||||
// todo 靠后完善
|
||||
// page.value = `/app/pages/index/diy_form?form_id=${formData.form_id}`
|
||||
// loadQrcode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认
|
||||
*/
|
||||
const confirm = () => {
|
||||
if(formData.tips_type == 'diy' && !formData.tips_text){
|
||||
ElMessage.error('提示不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (repeat.value) return;
|
||||
repeat.value = true
|
||||
|
||||
const data = formData
|
||||
|
||||
editDiyFormSubmitConfig(data).then(res => {
|
||||
repeat.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
}).catch(err => {
|
||||
repeat.value = false
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview-wrap {
|
||||
position: relative;
|
||||
width: 324px;
|
||||
min-height: 555px;
|
||||
padding: 82px 12px 20px;
|
||||
background-size: 100%;
|
||||
background-repeat: repeat-y;
|
||||
background-image: url(../../../../app/assets/images/diy_form/mobile_line.png);
|
||||
border-radius: 38px;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
background-color: #fff !important;
|
||||
margin-right: 24px;
|
||||
overflow-y: auto;
|
||||
|
||||
.page-wrap {
|
||||
position: relative;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
text-align: center;
|
||||
min-height: 548px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.verify-voucher-wrap {
|
||||
background-color: #f4f4f4;
|
||||
.tips-wrap{
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 21px;
|
||||
color: rgba(0,0,0,.65);
|
||||
margin: 20px 10px;
|
||||
}
|
||||
.qrcode-wrap{
|
||||
border-radius: 12px;
|
||||
margin: 0 20px;
|
||||
background: #fff;
|
||||
padding: 20px 10px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-wrap {
|
||||
padding: 20px 24px 24px;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1px;
|
||||
width: calc(100% - 48px);
|
||||
background-color: hsla(210, 8%, 51%, .13);
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&:last-child:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
341
admin/src/app/views/diy_form/components/form-write-popup.vue
Normal file
@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('writeSet')" width="600px" class="diy-dialog-wrap" :close-on-press-escape="true" :destroy-on-close="true" :close-on-click-modal="false">
|
||||
|
||||
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
|
||||
<!-- <el-form-item :label="t('填写方式')">-->
|
||||
<!-- <el-radio-group v-model="formData.write_way">-->
|
||||
<!-- <el-radio label="no_limit">{{t('不限制')}}</el-radio>-->
|
||||
<!-- <el-radio label="scan">{{t('仅限扫一扫')}}</el-radio>-->
|
||||
<!-- <el-radio label="url">{{t('仅限链接进入')}}</el-radio>-->
|
||||
<!-- </el-radio-group>-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<el-form-item :label="t('joinMemberType')">
|
||||
<el-radio-group v-model="formData.join_member_type">
|
||||
<el-radio label="all_member">{{t('allMember')}}</el-radio>
|
||||
<el-radio label="selected_member_level">{{t('selectedMemberLevel')}}</el-radio>
|
||||
<el-radio label="selected_member_label">{{t('selectedMemberLabel')}}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 会员标签 -->
|
||||
<el-form-item :label="t('memberLabel')" prop="label_ids" v-if="formData.join_member_type=='selected_member_label'">
|
||||
<el-select v-model="formData.label_ids" clearable multiple :placeholder="t('memberLabelPlaceholder')" class="input-width">
|
||||
<el-option :label="item['label_name']" :value="item['label_id']" v-for="(item, index) in labelSelectData" :key="index" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 会员等级 -->
|
||||
<el-form-item :label="t('memberLevel')" prop="level_ids" v-if="formData.join_member_type=='selected_member_level'">
|
||||
<el-select v-model="formData.level_ids" clearable multiple :placeholder="t('memberLevelPlaceholder')" class="input-width">
|
||||
<el-option :label="item['level_name']" :value="item['level_id']" v-for="(item, index) in levelSelectData" :key="index" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('apieceFillQuantity')" :class="{ '!mb-[5px]' : formData.member_write_type == 'diy' }">
|
||||
<el-radio-group v-model="formData.member_write_type">
|
||||
<el-radio label="no_limit">{{t('noLimit')}}</el-radio>
|
||||
<el-radio label="diy">{{t('diy')}}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label=" " v-if="formData.member_write_type == 'diy'" prop="member_write_rule">
|
||||
<div class="flex items-center">
|
||||
<span>每</span>
|
||||
<el-input v-model.trim="formData.member_write_rule.time_value" @keyup="filterNumber($event)" size="small" class="!w-[50px] px-[5px]" maxlength="3" />
|
||||
<el-select v-model="formData.member_write_rule.time_unit" class="!w-[60px] pr-[5px]" size="small">
|
||||
<el-option v-for="(item, index) in validityOptions" :key="item.value" :label="item.text" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
<span>可填写</span>
|
||||
<el-input v-model.trim="formData.member_write_rule.num" @keyup="filterNumber($event)" size="small" class="!w-[50px] px-[5px]" maxlength="3" />
|
||||
<span>次</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('fillQuantityTotal')" :class="{ '!mb-[5px]' : formData.form_write_type == 'diy' }">
|
||||
<el-radio-group v-model="formData.form_write_type">
|
||||
<el-radio label="no_limit">{{t('noLimit')}}</el-radio>
|
||||
<el-radio label="diy">{{t('diy')}}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label=" " v-if="formData.form_write_type == 'diy'" prop="form_write_rule">
|
||||
<div class="flex items-center">
|
||||
<span>每</span>
|
||||
<el-input v-model.trim="formData.form_write_rule.time_value" @keyup="filterNumber($event)" size="small" class="!w-[50px] px-[5px]" maxlength="3" />
|
||||
<el-select v-model="formData.form_write_rule.time_unit" class="!w-[60px] pr-[5px]" size="small">
|
||||
<el-option v-for="(item, index) in validityOptions" :key="item.value" :label="item.text" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
<span>可填写</span>
|
||||
<el-input v-model.trim="formData.form_write_rule.num" @keyup="filterNumber($event)" size="small" class="!w-[50px] px-[5px]" maxlength="3" />
|
||||
<span class="mr-[5px]">次</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p class="w-[250px]">{{ t('writeTips') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('fillInTheTimePeriod')" prop="time_limit_rule">
|
||||
<el-radio-group v-model="formData.time_limit_type">
|
||||
<el-radio label="no_limit">{{t('noLimit')}}</el-radio>
|
||||
<el-radio label="specify_time">{{t('setSpecifyTime')}}</el-radio>
|
||||
<el-radio label="open_day_time">{{t('openDayTime')}}</el-radio>
|
||||
</el-radio-group>
|
||||
<el-date-picker v-if="formData.time_limit_type == 'specify_time'" v-model="formData.time_limit_rule.specify_time" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
|
||||
<div class="flex items-center mt-[5px]" v-if="formData.time_limit_type == 'open_day_time'">
|
||||
<span class="mr-[5px]">每天</span>
|
||||
<el-time-picker class="!w-[180px]" v-model="formData.time_limit_rule.open_day_time" format="HH:mm" value-format="HH:mm" is-range range-separator="-" start-placeholder="开始时间" end-placeholder="结束时间" />
|
||||
<span class="ml-[5px]">可填写</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item :label="t('允许修改内容')" class="display-block">-->
|
||||
<!-- <el-switch v-model="formData.is_allow_update_content" :active-value="1" :inactive-value="0" />-->
|
||||
<!-- <div class="text-sm text-gray-400">{{ t('开启后,填表人可以修改自己填写的内容。') }}</div>-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<!-- <el-form-item :label="t('填写须知')">-->
|
||||
<!-- <el-input v-model.trim="formData.write_instruction" :placeholder="t('请输入填写须知')" type="textarea" maxlength="500" show-word-limit rows="5" class="w-[400px]" clearable />-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { filterNumber } from '@/utils/common'
|
||||
import {getMemberLabelAll,getMemberLevelAll } from '@/app/api/member'
|
||||
import { getFormWriteConfig,editDiyFormWriteConfig } from '@/app/api/diy_form'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: 0,
|
||||
form_id: 0, // 万能表单id
|
||||
write_way: 'no_limit', // 填写方式,no_limit:不限制,scan:仅限微信扫一扫,url:仅限链接进入
|
||||
join_member_type: 'all_member', // 参与会员,all_member:所有会员参与,selected_member_level:指定会员等级,selected_member_label:指定会员标签
|
||||
level_ids: [], // 会员等级id集合
|
||||
label_ids: [], // 会员标签id集合
|
||||
member_write_type: 'no_limit', // 每人可填写次数,no_limit:不限制,diy:自定义
|
||||
// 每人可填写次数自定义规则
|
||||
member_write_rule: {
|
||||
time_value: 1, // 时间
|
||||
time_unit: 'day', // 时间单位
|
||||
num: 1 // 可填写次数
|
||||
},
|
||||
form_write_type: 'no_limit', // 表单可填写数量,no_limit:不限制,diy:自定义
|
||||
// 表单可填写总数自定义规则
|
||||
form_write_rule: {
|
||||
time_value: 1, // 时间
|
||||
time_unit: 'day', // 时间单位
|
||||
num: 1 // 可填写次数
|
||||
},
|
||||
time_limit_type: 'no_limit', // 填写时间限制类型,no_limit:不限制,specify_time:指定开始结束时间,open_day_time:设置每日开启时间
|
||||
// 填写时间限制规则
|
||||
time_limit_rule: {
|
||||
specify_time: [], // 指定开始结束时间
|
||||
open_day_time: [], // 设置每日开启时间
|
||||
},
|
||||
is_allow_update_content: 0, // 是否允许修改自己填写的内容,0:否,1:是
|
||||
write_instruction: '', // 表单填写须知
|
||||
}
|
||||
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
label_ids: [
|
||||
{ required: true, message: t('labelTips'), trigger: 'blur' }
|
||||
],
|
||||
level_ids: [
|
||||
{ required: true, message: t('levelTips'), trigger: 'blur' }
|
||||
],
|
||||
member_write_rule: [
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
let unit = ''
|
||||
validityOptions.forEach((item,index)=>{
|
||||
if(item.value == value.time_unit){
|
||||
unit = item.text;
|
||||
}
|
||||
})
|
||||
if(formData.member_write_type == 'diy'){
|
||||
if(!value.time_value){
|
||||
callback(new Error(`${unit}数不能为空`))
|
||||
}else if(!value.num){
|
||||
callback(new Error(t('numCannotNull')))
|
||||
}else{
|
||||
callback()
|
||||
}
|
||||
}else{
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
form_write_rule: [
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
let unit = ''
|
||||
validityOptions.forEach((item,index)=>{
|
||||
if(item.value == value.time_unit){
|
||||
unit = item.text;
|
||||
}
|
||||
})
|
||||
if(formData.member_write_type == 'diy'){
|
||||
if(!value.time_value){
|
||||
callback(new Error(`${unit}数不能为空`))
|
||||
}else if(!value.num){
|
||||
callback(new Error(t('numCannotNull')))
|
||||
}else{
|
||||
callback()
|
||||
}
|
||||
}else{
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
time_limit_rule: [
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (formData.time_limit_type == 'specify_time' && (!value.specify_time || !value.specify_time.length)) {
|
||||
callback(new Error(t('timeLimitRuleOne')))
|
||||
} else if (formData.time_limit_type == 'open_day_time' && (!value.open_day_time || !value.open_day_time.length)) {
|
||||
callback(new Error(t('timeLimitRuleTwo')))
|
||||
} else if (formData.time_limit_type == 'open_day_time' && value.open_day_time && value.open_day_time.length) {
|
||||
if (value.open_day_time[0] == value.open_day_time[1]) {
|
||||
callback(new Error(t('timeLimitRuleThree')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const levelSelectData = ref([])
|
||||
const labelSelectData = ref([])
|
||||
|
||||
// 获取全部标签
|
||||
getMemberLabelAll().then(({ data }) => {
|
||||
labelSelectData.value = data
|
||||
})
|
||||
|
||||
getMemberLevelAll().then(({ data }) => {
|
||||
levelSelectData.value = data
|
||||
})
|
||||
|
||||
const validityOptions = reactive([
|
||||
{
|
||||
text:'天',
|
||||
value:'day'
|
||||
},
|
||||
{
|
||||
text:'周',
|
||||
value:'week'
|
||||
},
|
||||
{
|
||||
text:'月',
|
||||
value:'month'
|
||||
},
|
||||
{
|
||||
text:'年',
|
||||
value:'year'
|
||||
}
|
||||
])
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
Object.assign(formData, initialFormData)
|
||||
loading.value = true
|
||||
if (row) {
|
||||
const data = await (await getFormWriteConfig(row.form_id)).data
|
||||
if (data && Object.keys(data).length) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
}else{
|
||||
formData.form_id = row.form_id;
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
const data = formData
|
||||
|
||||
editDiyFormWriteConfig(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
}).catch(err => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const filterSpecial = (event:any) => {
|
||||
event.target.value = event.target.value.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '')
|
||||
event.target.value = event.target.value.replace(/[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、]/g, '')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss">
|
||||
.diy-dialog-wrap .el-form-item__label{
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.display-block {
|
||||
.el-form-item__content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1021
admin/src/app/views/diy_form/edit.vue
Normal file
584
admin/src/app/views/diy_form/list.vue
Normal file
@ -0,0 +1,584 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
<el-button type="primary" class="w-[100px]" @click="dialogVisible = true">{{ t('addDiyForm') }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
|
||||
<el-form :inline="true" :model="diyFormTableData.searchParam" ref="searchFormDiyFormRef">
|
||||
<el-form-item :label="t('title')" prop="title">
|
||||
<el-input v-model.trim="diyFormTableData.searchParam.title" :placeholder="t('titlePlaceholder')" />
|
||||
</el-form-item>
|
||||
<!-- <el-form-item :label="t('forAddon')" prop="addon_name">-->
|
||||
<!-- <el-select v-model="diyFormTableData.searchParam.addon_name" :placeholder="t('forAddonPlaceholder')" @change="handleSelectAddonChange">-->
|
||||
<!-- <el-option :label="t('all')" value="" />-->
|
||||
<!-- <el-option v-for="(item, key) in apps" :label="item.title" :value="key" :key="key"/>-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item :label="t('typeName')" prop="type">
|
||||
<el-select v-model="diyFormTableData.searchParam.type" :placeholder="t('formTypePlaceholder')">
|
||||
<el-option :label="t('all')" value="" />
|
||||
<el-option v-for="(item, key) in formType" :label="item.title" :value="key" :key="key"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadDiyFormList()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormDiyFormRef)">{{ t('reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<div class="mb-[10px] flex items-center">
|
||||
<el-checkbox v-model="toggleCheckbox" size="large" class="px-[14px]" @change="toggleChange" :indeterminate="isIndeterminate" />
|
||||
<el-button @click="batchDeleteForms" size="small">{{t("batchDeletion")}}</el-button>
|
||||
</div>
|
||||
<el-table :data="diyFormTableData.data" size="large" ref="diyFormListTableRef" v-loading="diyFormTableData.loading" @selection-change="handleSelectionChange">
|
||||
<template #empty>
|
||||
<span>{{ !diyFormTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="page_title" :label="t('title')" min-width="120" />
|
||||
<!-- <el-table-column prop="addon_name" :label="t('forAddon')" min-width="80" />-->
|
||||
<el-table-column prop="type_name" :label="t('typeName')" min-width="80" />
|
||||
<el-table-column :label="t('status')" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="success" v-if="row.status == 1" class="cursor-pointer" @click="showClick(row)">{{ t('statusOn') }}</el-tag>
|
||||
<el-tag type="info" v-else class="cursor-pointer" @click="showClick(row)">{{ t('statusOff') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="update_time" :label="t('updateTime')" min-width="120" />
|
||||
|
||||
<el-table-column :label="t('operation')" fixed="right" align="right" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center justify-end">
|
||||
<el-button type="primary" v-if="row.status == 1 && row.type=='DIY_FORM'" link @click="spreadEvent(row)">{{ t('promotion') }}</el-button>
|
||||
<!-- <el-button type="primary" link @click="toPreview(row)">{{ t('preview') }}</el-button>-->
|
||||
<el-button type="primary" link @click="editEvent(row)">{{ t('edit') }}</el-button>
|
||||
<el-button v-if="row.status == 0" type="primary" link @click="deleteEvent(row.form_id)">{{ t('delete') }}</el-button>
|
||||
<el-button type="primary" link @click="detailEvent(row)">{{ t('detail') }}</el-button>
|
||||
<el-dropdown placement="bottom" trigger="click" class="ml-[12px]">
|
||||
<el-button type="primary" link>{{ t('more') }}</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="row.type=='DIY_FORM'">
|
||||
<el-button type="primary" class="w-full" link @click="submitConfigEvent(row)">{{ t('submitSuccess') }}</el-button>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<el-button type="primary" class="w-full" link @click="writeConfigEvent(row)">{{ t('writeSet') }}</el-button>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.type=='DIY_FORM'">
|
||||
<el-button type="primary" class="w-full" link @click="openShare(row)">{{ t('shareSet') }}</el-button>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<el-button type="primary" class="w-full" link @click="exportEvent(row)">{{ t('export') }}</el-button>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<el-button type="primary" class="w-full" link @click="copyEvent(row.form_id)">{{ t('copy') }}</el-button>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="diyFormTableData.page" v-model:page-size="diyFormTableData.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="diyFormTableData.total"
|
||||
@size-change="loadDiyFormList()" @current-change="loadDiyFormList" />
|
||||
</div>
|
||||
|
||||
</el-card>
|
||||
|
||||
<!--添加表单-->
|
||||
<el-dialog v-model="dialogVisible" :title="t('addFormTips')" width="980px">
|
||||
<el-form :model="formData" ref="formRef" :rules="formRules">
|
||||
<!-- <el-form-item :label="t('title')" prop="title">-->
|
||||
<!-- <el-input v-model.trim="formData.title" :placeholder="t('titlePlaceholder')" clearable maxlength="12" show-word-limit class="w-full" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item prop="type">
|
||||
<div class="image-selection-container">
|
||||
<div
|
||||
v-for="(item, key) in formType"
|
||||
:key="key"
|
||||
class="image-option"
|
||||
:class="{ selected: formData.type === key }"
|
||||
@click="selectType(key)"
|
||||
>
|
||||
<img :src="img(item.preview)" class="option-image" />
|
||||
<div class="option-title">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <el-select v-model="formData.type" :placeholder="t('formTypePlaceholder')" class="!w-full">
|
||||
<el-option v-for="(item, key) in formType" :label="item.title" :value="key" :key="key"/>
|
||||
</el-select> -->
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="addEvent(formRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分享设置-->
|
||||
<el-dialog v-model="shareDialogVisible" :title="t('shareSet')" width="30%">
|
||||
<el-tabs v-model="tabShareType">
|
||||
<el-tab-pane :label="t('wechat')" name="wechat"></el-tab-pane>
|
||||
<el-tab-pane :label="t('weapp')" name="weapp"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-form :model="shareFormData[tabShareType]" label-width="90px" ref="shareFormRef" :rules="shareFormRules">
|
||||
<el-form-item :label="t('sharePage')">
|
||||
<span>{{ sharePage }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('shareTitle')" prop="title">
|
||||
<el-input v-model.trim="shareFormData[tabShareType].title" :placeholder="t('shareTitlePlaceholder')" clearable maxlength="30" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('shareDesc')" prop="desc" v-if="tabShareType == 'wechat'">
|
||||
<el-input v-model.trim="shareFormData[tabShareType].desc" :placeholder="t('shareDescPlaceholder')" type="textarea" rows="4" clearable maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('shareImageUrl')" prop="url">
|
||||
<upload-image v-model="shareFormData[tabShareType].url" :limit="1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="shareDialogVisible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="shareEvent(shareFormRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 推广弹出框 -->
|
||||
<form-spread-popup ref="formSpreadPopupRef" />
|
||||
|
||||
<!-- 表单提交成功页弹出框 -->
|
||||
<form-submit-popup ref="formSubmitPopupRef" @complete="loadDiyFormList" />
|
||||
|
||||
<!-- 表单填写设置弹出框 -->
|
||||
<form-write-popup ref="formWritePopupRef" @complete="loadDiyFormList" />
|
||||
|
||||
<records-detail ref="recordsDetailDialog"/>
|
||||
|
||||
<!-- 表单明细导出弹出框 -->
|
||||
<export-sure ref="exportSureDialog" :show="flag" type="diy_form_records" :searchParam="diyFormDetailData" @close="handleExportClose" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getFormType, getApps, getDiyFormPageList, deleteDiyForm, editDiyFormShare, editFormStatus, copyForm } from '@/app/api/diy_form'
|
||||
import { FormInstance, ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { setTablePageStorage,getTablePageStorage } from "@/utils/common";
|
||||
import { img } from '@/utils/common'
|
||||
import recordsDetail from '@/app/views/diy_form/records.vue'
|
||||
import formSpreadPopup from '@/app/views/diy_form/components/form-spread-popup.vue'
|
||||
import formSubmitPopup from '@/app/views/diy_form/components/form-submit-popup.vue'
|
||||
import formWritePopup from '@/app/views/diy_form/components/form-write-popup.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
const repeat = ref(false)
|
||||
|
||||
const formType: any = reactive({}) // 表单类型
|
||||
|
||||
// 添加自定义表单
|
||||
const formData = reactive({
|
||||
title: '',
|
||||
type: ''
|
||||
})
|
||||
|
||||
//详情
|
||||
const recordsDetailDialog: Record<string, any> | null = ref(null)
|
||||
const detailEvent=(row: any)=>{
|
||||
let data = {form_id: row.form_id};
|
||||
recordsDetailDialog.value.setFormData(data);
|
||||
recordsDetailDialog.value.showDialog = true;
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
title: [
|
||||
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: t('formTypePlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const dialogVisible = ref(false)
|
||||
const addEvent = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate(async(valid) => {
|
||||
if (valid) {
|
||||
const query = { type: formData.type } // , title: formData.title
|
||||
const url = router.resolve({
|
||||
path: '/decorate/form/edit',
|
||||
query
|
||||
})
|
||||
window.open(url.href)
|
||||
dialogVisible.value = false
|
||||
formData.title = ''
|
||||
formData.type = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showClick = (row: any) => {
|
||||
const data = row.status === 1 ? 0 : 1
|
||||
const obj = {
|
||||
form_id: row.form_id,
|
||||
status: data
|
||||
}
|
||||
editFormStatus(obj).then((res) => {
|
||||
row.status = data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取万能表单类型
|
||||
const loadFormType = (addon = '')=> {
|
||||
getFormType({}).then(res => {
|
||||
for (let key in formType) {
|
||||
delete formType[key];
|
||||
}
|
||||
|
||||
for (const key in res.data) {
|
||||
formType[key] = res.data[key]
|
||||
}
|
||||
formData.type = Object.keys(formType)[0]
|
||||
})
|
||||
}
|
||||
|
||||
loadFormType();
|
||||
|
||||
const apps: any = reactive({}) // 应用插件列表
|
||||
|
||||
// todo 靠后完善
|
||||
// getApps({}).then(res=>{
|
||||
// if(res.data){
|
||||
// for (const key in res.data) {
|
||||
// apps[key] = res.data[key];
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
// 根据所属插件,查询表单类型
|
||||
const handleSelectAddonChange = (value: any) => {
|
||||
diyFormTableData.searchParam.type = '';
|
||||
loadFormType(value)
|
||||
}
|
||||
|
||||
const diyFormTableData: any = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
title: '',
|
||||
type: '',
|
||||
mode: '',
|
||||
addon_name: ''
|
||||
}
|
||||
})
|
||||
|
||||
const searchFormDiyFormRef = ref<FormInstance>()
|
||||
|
||||
// 获取自定义表单列表
|
||||
const loadDiyFormList = (page: number = 1) => {
|
||||
diyFormTableData.loading = true
|
||||
diyFormTableData.page = page
|
||||
|
||||
getDiyFormPageList({
|
||||
page: diyFormTableData.page,
|
||||
limit: diyFormTableData.limit,
|
||||
...diyFormTableData.searchParam
|
||||
}).then(res => {
|
||||
diyFormTableData.loading = false
|
||||
diyFormTableData.data = res.data.data
|
||||
diyFormTableData.total = res.data.total
|
||||
setTablePageStorage(diyFormTableData.page, diyFormTableData.limit, diyFormTableData.searchParam);
|
||||
}).catch(() => {
|
||||
diyFormTableData.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
loadDiyFormList(getTablePageStorage(diyFormTableData.searchParam).page);
|
||||
|
||||
const selectType = (index: number) => {
|
||||
formData.type = index.toString();
|
||||
}
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
loadDiyFormList()
|
||||
}
|
||||
|
||||
// 编辑自定义表单
|
||||
const editEvent = (data: any) => {
|
||||
const url = router.resolve({
|
||||
path: '/decorate/form/edit',
|
||||
query: { form_id: data.form_id }
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
// 复制页面
|
||||
const copyEvent = (id: any) => {
|
||||
ElMessageBox.confirm(t('diyFormCopyTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
if (repeat.value) return
|
||||
repeat.value = true
|
||||
|
||||
copyForm({form_id: id}).then((res: any) => {
|
||||
if (res.code == 1) {
|
||||
loadDiyFormList()
|
||||
}
|
||||
repeat.value = false
|
||||
}).catch(() => {
|
||||
repeat.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 删除自定义表单
|
||||
const deleteEvent = (form_id: number) => {
|
||||
ElMessageBox.confirm(t('diyFormDeleteTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
deleteDiyForm({ form_ids: [form_id] }).then(() => {
|
||||
loadDiyFormList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
// 批量复选框
|
||||
const toggleCheckbox = ref();
|
||||
|
||||
// 复选框中间状态
|
||||
const isIndeterminate = ref(false);
|
||||
|
||||
// 监听批量复选框事件
|
||||
const toggleChange = (value: any) => {
|
||||
isIndeterminate.value = false;
|
||||
diyFormListTableRef.value.toggleAllSelection();
|
||||
};
|
||||
|
||||
const diyFormListTableRef = ref();
|
||||
|
||||
// 选中数据
|
||||
const multipleSelection: any = ref([]);
|
||||
|
||||
// 监听表格单行选中
|
||||
const handleSelectionChange = (val: []) => {
|
||||
multipleSelection.value = val;
|
||||
|
||||
toggleCheckbox.value = false;
|
||||
if (
|
||||
multipleSelection.value.length > 0 &&
|
||||
multipleSelection.value.length < diyFormTableData.data.length
|
||||
) {
|
||||
isIndeterminate.value = true;
|
||||
} else {
|
||||
isIndeterminate.value = false;
|
||||
}
|
||||
|
||||
if (multipleSelection.value.length == diyFormTableData.data.length) {
|
||||
toggleCheckbox.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const batchDeleteForms = () => {
|
||||
if (multipleSelection.value.length == 0) {
|
||||
ElMessage({
|
||||
type: "warning",
|
||||
message: `${t("batchEmptySelectedFormsTips")}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(t("batchFormsDeleteTips"), t("warning"), {
|
||||
confirmButtonText: t("confirm"),
|
||||
cancelButtonText: t("cancel"),
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
if (repeat.value) return;
|
||||
repeat.value = true;
|
||||
|
||||
const form_ids: any = [];
|
||||
multipleSelection.value.forEach((item: any) => {
|
||||
form_ids.push(item.form_id);
|
||||
});
|
||||
|
||||
deleteDiyForm({
|
||||
form_ids: form_ids,
|
||||
}).then(() => {
|
||||
loadDiyFormList();
|
||||
repeat.value = false;
|
||||
}).catch(() => {
|
||||
repeat.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转去预览
|
||||
const toPreview = (data: any) => {
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page: '/app/pages/index/diy_form?form_id=' + data.form_id
|
||||
}
|
||||
});
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
const tabShareType = ref('wechat')
|
||||
const sharePage = ref('')
|
||||
const shareFormId = ref(0)
|
||||
const shareFormData = reactive({
|
||||
wechat: {
|
||||
title: '',
|
||||
desc: '',
|
||||
url: ''
|
||||
},
|
||||
weapp: {
|
||||
title: '',
|
||||
url: ''
|
||||
}
|
||||
})
|
||||
|
||||
const shareDialogVisible = ref(false)
|
||||
const shareFormRules = computed(() => {
|
||||
return {}
|
||||
})
|
||||
|
||||
const shareFormRef = ref<FormInstance>()
|
||||
const openShare = async (row: any) => {
|
||||
shareFormId.value = row.form_id
|
||||
sharePage.value = row.title
|
||||
const share = row.share
|
||||
shareFormData.wechat = share.wechat
|
||||
shareFormData.weapp = share.weapp
|
||||
|
||||
shareDialogVisible.value = true
|
||||
}
|
||||
|
||||
const shareEvent = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
editDiyFormShare({
|
||||
form_id: shareFormId.value,
|
||||
share: JSON.stringify(shareFormData)
|
||||
}).then(() => {
|
||||
loadDiyFormList()
|
||||
shareDialogVisible.value = false
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 表单推广
|
||||
const formSpreadPopupRef: any = ref(null)
|
||||
|
||||
const spreadEvent = (data: any) => {
|
||||
formSpreadPopupRef.value.show(data)
|
||||
}
|
||||
|
||||
// 表单提交成功页弹出框
|
||||
const formSubmitPopupRef: any = ref(null)
|
||||
|
||||
const submitConfigEvent = (data: any) => {
|
||||
formSubmitPopupRef.value.setFormData(data)
|
||||
formSubmitPopupRef.value.showDialog = true
|
||||
}
|
||||
|
||||
// 表单填写设置弹出框
|
||||
const formWritePopupRef: any = ref(null)
|
||||
|
||||
const writeConfigEvent = (data: any) => {
|
||||
formWritePopupRef.value.setFormData(data)
|
||||
formWritePopupRef.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单填写记录明细导出
|
||||
*/
|
||||
const exportSureDialog = ref(null)
|
||||
const flag = ref(false)
|
||||
const handleExportClose = (val) => {
|
||||
flag.value = val
|
||||
}
|
||||
|
||||
const diyFormDetailData: any = reactive({
|
||||
form_id: 0,
|
||||
})
|
||||
const exportEvent = (data: any) => {
|
||||
diyFormDetailData.form_id = data.form_id
|
||||
flag.value = true
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.image-selection-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
.image-option {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 300px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 10px;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.image-option.selected {
|
||||
border-color: var(--el-color-primary);
|
||||
box-shadow: 0 0 10px var(--el-color-primary-light-9);
|
||||
}
|
||||
.option-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
.option-title {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
391
admin/src/app/views/diy_form/records.vue
Normal file
@ -0,0 +1,391 @@
|
||||
<template>
|
||||
<el-drawer v-model="showDialog" :title="t('dataAndStatistics')" direction="rtl" size="70%" :before-close="handleClose" class="member-detail-drawer">
|
||||
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
|
||||
<el-tab-pane :label="t('detailData')" name="detail_data">
|
||||
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
|
||||
<el-form :inline="true" :model="formData.searchParam" ref="searchFormDiyFormRef">
|
||||
<el-form-item :label="t('fillInFormPerson')" prop="keyword">
|
||||
<el-input v-model.trim="formData.searchParam.keyword" :placeholder="t('fillInFormPersonplaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('fillInFormDate')" prop="create_time">
|
||||
<el-date-picker v-model="formData.searchParam.create_time" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" :start-placeholder="t('startDate')" :end-placeholder="t('endDate')" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadFormRecordsListFn()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormDiyFormRef)">{{ t('reset') }}</el-button>
|
||||
<el-button type="primary" @click="exportEvent">{{ t('export') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-table :data="formData.data" size="large" v-loading="formData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !formData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column fixed :label="t('fillInFormPersonInfo')" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center cursor-pointer" @click="detailEvent(row.member.member_id)">
|
||||
<div class="min-w-[50px] h-[50px] flex items-center justify-center">
|
||||
<el-image v-if="row.member.headimg" class="w-[50px] h-[50px]" :src="img(row.member.headimg)" fit="contain">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<img class="w-[50px] h-[50px] rounded-full" src="@/app/assets/images/member_head.png" alt="">
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<img class="w-[50px] h-[50px] rounded-full" v-else src="@/app/assets/images/member_head.png" alt="">
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<span :title="(row.member.nickname || row.member.username)" class="multi-hidden">{{row.member.nickname || row.member.username}}</span>
|
||||
<span class="text-primary text-[12px]">{{row.member.mobile || ''}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed prop="create_time" :label="t('fillInFormDate')" min-width="120" />
|
||||
|
||||
<el-table-column v-for="item in formFieldsList" :key="item.field_key" :label="item.field_name" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<!-- 动态渲染表单组件内容 -->
|
||||
<template v-if="row.recordsFieldList[item.field_key]">
|
||||
<component :is="row.recordsFieldList[item.field_key].detailComponent" :data="row.recordsFieldList[item.field_key]"/>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('operation')" fixed="right" align="right" min-width="70">
|
||||
<template #default="{ row }">
|
||||
<!-- <el-button type="primary" link @click="formDetailEvent(row)">{{ t('详情') }}</el-button> -->
|
||||
<el-button type="primary" link @click="deleteEvent(row)">{{ t('delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="formData.page" v-model:page-size="formData.limit" :page-sizes="[6,10,20,30,50,100]"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="formData.total"
|
||||
@size-change="loadFormRecordsListFn()" @current-change="loadFormRecordsListFn" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('fillInFormPersonStatics')" name="member_stat">
|
||||
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
|
||||
<el-form :inline="true" :model="formMemberList.searchParam" ref="searchFormDiyMemberRef">
|
||||
<el-form-item :label="t('fillInFormPerson')" prop="keyword">
|
||||
<el-input v-model.trim="formMemberList.searchParam.keyword" :placeholder="t('fillInFormPersonplaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getFormRecordsMemberFn()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetFormMember(searchFormDiyMemberRef)">{{ t('reset') }}</el-button>
|
||||
<el-button type="primary" @click="exportMemberEvent">{{ t('export') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-table :data="formMemberList.data" size="large" v-loading="formMemberList.loading">
|
||||
<template #empty>
|
||||
<span>{{ !formMemberList.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column fixed :label="t('fillInFormPersonInfo')" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center cursor-pointer" @click="detailEvent(row.member.member_id)">
|
||||
<div class="min-w-[50px] h-[50px] flex items-center justify-center">
|
||||
<el-image v-if="row.member.headimg" class="w-[50px] h-[50px]" :src="img(row.member.headimg)" fit="contain">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<img class="w-[50px] h-[50px] rounded-full" src="@/app/assets/images/member_head.png" alt="">
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<img class="w-[50px] h-[50px] rounded-full" v-else src="@/app/assets/images/member_head.png" alt="">
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<span :title="(row.member.nickname || row.member.username)" class="multi-hidden">{{row.member.nickname || row.member.username}}</span>
|
||||
<span class="text-primary text-[12px]">{{row.member.mobile || ''}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column fixed prop="create_time" :label="t('填表时间')" min-width="120" /> -->
|
||||
<el-table-column fixed prop="create_time" :label="t('fillInFormTotal')" min-width="500">
|
||||
<template #default="{ row }" @click="">
|
||||
{{ row.write_count }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="formMemberList.page" v-model:page-size="formMemberList.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="formMemberList.total"
|
||||
@size-change="getFormRecordsMemberFn()" @current-change="getFormRecordsMemberFn()" />
|
||||
</div>
|
||||
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('fieldStatistics')" name="field_stat">
|
||||
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
|
||||
<el-form :inline="true" :model="formData.searchParam" ref="searchFormDiyFieldsRef">
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="exportFieldsEvent">{{ t('export') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-collapse v-model="activeNames" class="diy-collapse mt-[15px]">
|
||||
<el-collapse-item :title="item.field_name" :name="item.field_id" v-for="(item, index) in formFieldsStat" :key="index">
|
||||
<template #title>
|
||||
<div class="text-[16px] font-bold">{{item.field_name}}</div>
|
||||
</template>
|
||||
<el-table :data="item.value_list" border>
|
||||
<el-table-column :label="item.field_name" prop="render_value">
|
||||
<template #default="{ row }">
|
||||
{{row.render_value ? row.render_value : '未填写'}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="小计" prop="write_count"></el-table-column>
|
||||
<el-table-column label="比例">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="row.write_percent"></el-progress>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="t('viewInformation')" width="400px">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex mb-[10px]" v-for="(item, index) in formDetail" :key="index">
|
||||
<div class="flex justify-end w-[100px]">{{ item.label }}:</div>
|
||||
<div class="flex ml-[20px]">
|
||||
<div v-if="Array.isArray(item.text)" class="mr-[10px]" v-for="(textItem, i) in item.text" :key="i">
|
||||
{{ textItem }}
|
||||
</div>
|
||||
<div v-else>{{ item.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="dialogVisible = false">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</el-drawer>
|
||||
<export-sure ref="exportSureDialog" :show="flag" type="diy_form_records" :searchParam="formData.searchParam" @close="handleExportClose" />
|
||||
<export-sure ref="exportSureDialog" :show="flagMember" type="diy_form_records_member" :searchParam="formMemberList.searchParam" @close="handleMemberExportClose" />
|
||||
<export-sure ref="exportSureDialog" :show="flagFields" type="diy_form_records_fields" :searchParam="formData.searchParam" @close="handleFieldsExportClose" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, defineAsyncComponent } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getDiyFormFieldsList, getDiyFormFieldStat, getFormRecords,getFormRecordsInfo,deleteFormRecords,getFormRecordsMember} from '@/app/api/diy_form'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { img } from '@/utils/common'
|
||||
import { ElMessageBox, FormInstance } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const showDialog = ref(false)
|
||||
const activeName = ref('detail_data')
|
||||
const formId = ref(0)
|
||||
const dialogVisible = ref(false)
|
||||
const searchFormDiyFormRef = ref<FormInstance>()
|
||||
const searchFormDiyMemberRef = ref<FormInstance>()
|
||||
const searchFormDiyFieldsRef = ref<FormInstance>()
|
||||
const handleClose = (done: () => void) => {
|
||||
showDialog.value = false;
|
||||
}
|
||||
|
||||
const formData = reactive({
|
||||
page: 1,
|
||||
limit: 6,
|
||||
total: 0,
|
||||
loading: false,
|
||||
data: [],
|
||||
searchParam: {
|
||||
form_id: 0,
|
||||
keyword: '',
|
||||
create_time: ''
|
||||
}
|
||||
})
|
||||
|
||||
const formFieldsList = ref([])
|
||||
|
||||
// 获取万能表单字段列表
|
||||
const getDiyFormFieldsListFn = (form_id:any)=>{
|
||||
getDiyFormFieldsList({
|
||||
form_id,
|
||||
order: 'field_id',
|
||||
sort: 'asc'
|
||||
}).then((res:any)=>{
|
||||
formFieldsList.value = res.data;
|
||||
})
|
||||
}
|
||||
|
||||
// 获取字段统计列表
|
||||
const formFieldsStat = ref([])
|
||||
const getDiyFormFieldStatFn = (form_id:any)=>{
|
||||
getDiyFormFieldStat({
|
||||
form_id
|
||||
}).then((res:any)=>{
|
||||
formFieldsStat.value = res.data;
|
||||
})
|
||||
}
|
||||
|
||||
const modules: any = import.meta.glob('@/**/*.vue')
|
||||
const formDetail = ref([])
|
||||
|
||||
const formDetailEvent = (row: any) => {
|
||||
getFormRecordsInfo(row.record_id).then((res:any)=>{
|
||||
formDetail.value = res.data.value
|
||||
dialogVisible.value = true
|
||||
})
|
||||
}
|
||||
|
||||
// 删除
|
||||
const deleteEvent = (row: any) => {
|
||||
ElMessageBox.confirm(t('deleteTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
deleteFormRecords({
|
||||
record_id: row.record_id,
|
||||
form_id: row.form_id
|
||||
}).then(() => {
|
||||
initData();
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
loadFormRecordsListFn()
|
||||
}
|
||||
const resetFormMember = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
getFormRecordsMemberFn()
|
||||
}
|
||||
|
||||
const loadFormRecordsListFn= (page: number = 1)=>{
|
||||
formData.loading = true
|
||||
formData.page = page
|
||||
getFormRecords({
|
||||
page: formData.page,
|
||||
limit: formData.limit,
|
||||
...formData.searchParam
|
||||
}).then((res:any)=>{
|
||||
formData.loading = false
|
||||
formData.data = res.data.data
|
||||
formData.data.forEach((item:any)=>{
|
||||
for (let key:any in item.recordsFieldList){
|
||||
if (modules[item.recordsFieldList[key].detailComponent]) {
|
||||
item.recordsFieldList[key].detailComponent && (item.recordsFieldList[key].detailComponent = defineAsyncComponent(modules[item.recordsFieldList[key].detailComponent]))
|
||||
}
|
||||
}
|
||||
})
|
||||
formData.total = res.data.total
|
||||
}).catch(() => {
|
||||
formData.loading = false
|
||||
})
|
||||
}
|
||||
const formMemberList = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: false,
|
||||
data: [],
|
||||
searchParam: {
|
||||
keyword: '',
|
||||
form_id: 0,
|
||||
}
|
||||
})
|
||||
|
||||
const getFormRecordsMemberFn = (page: number = 1) => {
|
||||
formMemberList.loading = true
|
||||
formMemberList.page = page
|
||||
getFormRecordsMember({
|
||||
page: formMemberList.page,
|
||||
limit: formMemberList.limit,
|
||||
...formMemberList.searchParam
|
||||
}).then((res: any) => {
|
||||
formMemberList.data = res.data.data;
|
||||
formMemberList.total = res.total;
|
||||
formMemberList.loading = false;
|
||||
}).catch((error) => {
|
||||
formMemberList.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
//查看会员详情
|
||||
const detailEvent = (member_id:number)=> {
|
||||
let routeData = router.resolve(`/member/detail?id=${member_id}`)
|
||||
window.open(routeData.href, ' blank');
|
||||
}
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
formId.value = row.form_id;
|
||||
formData.searchParam.form_id = row.form_id;
|
||||
formMemberList.searchParam.form_id = row.form_id;
|
||||
|
||||
getDiyFormFieldsListFn(row.form_id);
|
||||
initData();
|
||||
}
|
||||
|
||||
const initData = () =>{
|
||||
getFormRecordsMemberFn();
|
||||
getDiyFormFieldStatFn(formId.value);
|
||||
loadFormRecordsListFn()
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单填写记录明细导出
|
||||
*/
|
||||
const exportSureDialog = ref(null)
|
||||
const flag = ref(false)
|
||||
const handleExportClose = (val) => {
|
||||
flag.value = val
|
||||
}
|
||||
const exportEvent = () => {
|
||||
flag.value = true
|
||||
}
|
||||
|
||||
const flagMember = ref(false)
|
||||
const handleMemberExportClose = (val) => {
|
||||
flagMember.value = val
|
||||
}
|
||||
const exportMemberEvent = () => {
|
||||
flagMember.value = true
|
||||
}
|
||||
|
||||
const flagFields = ref(false)
|
||||
const handleFieldsExportClose = (val) => {
|
||||
flagFields.value = val
|
||||
}
|
||||
const exportFieldsEvent = () => {
|
||||
flagFields.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.diy-collapse .el-collapse-item__header{
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
.el-icon.el-collapse-item__arrow{
|
||||
margin-left: inherit;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -2,7 +2,7 @@
|
||||
<el-container :class="['w-full h-screen bg-page flex flex-col', { 'login-wrap': loginType == 'admin' }, { 'site-login-wrap': loginType == 'site' }]">
|
||||
<!-- 平台端登录 -->
|
||||
<el-main class="login-main items-center justify-center flex-1 h-0" v-if="loginType == 'admin'">
|
||||
<div class="flex rounded-2xl overflow-hidden">
|
||||
<div class="flex rounded-2xl shadow overflow-hidden">
|
||||
<div class="login-main-left w-[450px] flex flex-wrap justify-center">
|
||||
<template v-if="loginConfig">
|
||||
<el-image v-if="loginConfig.bg&&!imgLoading" class="w-[450px] h-[400px]" :src="img(loginConfig.bg)" fit="cover" />
|
||||
@ -12,104 +12,73 @@
|
||||
<div class="login flex flex-col w-[400px] h-[400px] p-[40px]">
|
||||
<h3 class="text-center text-lg font-bold mb-[10px]">{{ webSite.site_name || t('siteTitle') }}</h3>
|
||||
<h3 class="text-center text-2xl font-bold mb-[26px]">{{ t('platform') }}</h3>
|
||||
<el-form :model="form" ref="formRef" :rules="formRules">
|
||||
<el-form :model="form" ref="formRef" :rules="formRules" class="mt-[30px]">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="form.username" :placeholder="t('userPlaceholder')"
|
||||
autocomplete="off"
|
||||
@keyup.enter="handleLogin(formRef)" class="h-[40px] input-with-select">
|
||||
<template #prepend>
|
||||
<icon name="element User" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input v-model.trim="form.username" :placeholder="t('userPlaceholder')" autocomplete="off" @keyup.enter="handleLogin(formRef)" class="h-[40px]"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" :placeholder="t('passwordPlaceholder')" type="password"
|
||||
autocomplete="new-password"
|
||||
@keyup.enter="handleLogin(formRef)" :show-password="true"
|
||||
class="h-[40px] input-with-select">
|
||||
<template #prepend>
|
||||
<icon name="element Lock" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-form-item prop="password" class="mt-[30px]">
|
||||
<el-input v-model.trim="form.password" :placeholder="t('passwordPlaceholder')" type="password" autocomplete="new-password" @keyup.enter="handleLogin(formRef)" :show-password="true" class="h-[40px]"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="mt-[30px] h-[40px] w-full" @click="handleLogin(formRef)" :loading="loading">{{ loading ? t('logging') : t('login') }}</el-button>
|
||||
<el-button type="primary" class="mt-[30px] !h-[40px] w-full" @click="handleLogin(formRef)" :loading="loading">{{ loading ? t('logging') : t('login') }}</el-button>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
|
||||
<!-- 站点端登录 -->
|
||||
<el-main class="login-main w-full login-site-main items-center h-screen justify-evenly bg-[#F8FAFF]" v-else-if="!imgLoading && loginType == 'site'">
|
||||
<div class="flex rounded-2xl overflow-hidden h-screen w-full relative">
|
||||
<template v-if="loginConfig">
|
||||
<img v-if="loginConfig.site_bg&&!imgLoading" class="hidden h-[100%] lg:block" :src="img(loginConfig.site_bg)" />
|
||||
<img v-else class="hidden h-[100%] lg:block" src="@/app/assets/images/site_login_bg.png" />
|
||||
</template>
|
||||
<div class="w-[100%] bg-[#F8FAFF] flex flex-col absolute right-0 top-0 h-screen lg:w-[60%]">
|
||||
<div class="flex justify-center items-center flex-1 h-0">
|
||||
<div class="site-login-item w-[45%] py-[30px] relative rounded-[13px] max-w-[350px] bg-[#fff]">
|
||||
<div class="w-[80%] mx-auto">
|
||||
<h3 class="text-3xl mb-[30px]">{{ t('siteLogin') }}</h3>
|
||||
<el-form :model="form" ref="formRef" :rules="formRules">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="form.username" @keyup.enter="handleLogin(formRef)" autocomplete="off" class="w-50 m-1 h-[40px]" :placeholder="t('userPlaceholder')">
|
||||
<template #prefix>
|
||||
<icon name="element User" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input type="password" v-model="form.password" @keyup.enter="handleLogin(formRef)"
|
||||
autocomplete="new-password" :show-password="true" class="w-50 m-1 h-[40px]" :placeholder="t('passwordPlaceholder')">
|
||||
<template #prefix>
|
||||
<icon name="element Lock" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="mt-[30px] h-[40px] w-full" @click="handleLogin(formRef)" :loading="loading">{{ loading ? t('logging') :
|
||||
t('login') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center mt-[20px] text-[#999] text-sm pb-8 " v-if="copyright">
|
||||
<a :href="copyright.gov_url" v-if="copyright.gov_record" class="flex" target="_blank">
|
||||
<img src="@/app/assets/images/gov_icon.png" alt="" class="h-[20px] mr-1">
|
||||
<span class="mr-3">公安备案号:{{ copyright.gov_record }}</span>
|
||||
</a>
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" v-if="copyright.icp">
|
||||
<span class="mr-3">备案号:{{ copyright.icp }}</span>
|
||||
</a>
|
||||
<a :href="copyright.copyright_link" target="_blank">
|
||||
<span class="mr-3" v-if="copyright.company_name">{{ copyright.company_name }}</span>
|
||||
<span class="mr-3" v-if="copyright.copyright_desc">©{{ copyright.copyright_desc }}</span>
|
||||
</a>
|
||||
<el-main class="login-main w-full mt-[120px] justify-center h-0" v-else-if="!imgLoading && loginType == 'site'">
|
||||
<div>
|
||||
<div class="login-main-left flex flex-col items-center justify-center">
|
||||
<div class="w-[130px] h-[40px] overflow-hidden" v-if="webSite.icon">
|
||||
<el-image class="w-full h-full" :src="img(webSite.icon)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-full"><img class="max-w-[130px]" src="@/app/assets/images/logo.default.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="text-[30px] mt-[10px]">{{ webSite.site_name || t('siteTitle') }}</div>
|
||||
</div>
|
||||
<div class="login flex flex-col w-[400px] mt-[60px] h-[350px] rounded-[10px] py-[20px] px-[40px]">
|
||||
<h3 class="text-center mt-[20px] text-[24px]">{{ t('欢迎登录') }}</h3>
|
||||
<el-form :model="form" ref="formRef" :rules="formRules" class="mt-[30px] formtwo">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model.trim="form.username" :placeholder="t('userPlaceholder')" autocomplete="off" :input-style="{boxShadow: 'none'}" @keyup.enter="handleLogin(formRef)" class="h-[40px]">
|
||||
<template #prefix>
|
||||
<img class="max-w-[20px]" src="@/app/assets/images/login/username.png" alt="" object-fit="contain">
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password" class="mt-[30px]">
|
||||
<el-input v-model.trim="form.password" :placeholder="t('passwordPlaceholder')" type="password" autocomplete="new-password" @keyup.enter="handleLogin(formRef)" :show-password="true" class="h-[40px]">
|
||||
<template #prefix>
|
||||
<img class="max-w-[20px]" src="@/app/assets/images/login/password.png" alt="" object-fit="contain">
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="mt-[30px] !h-[40px] w-full" @click="handleLogin(formRef)" :loading="loading">{{ loading ? t('logging') : t('login') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-main>
|
||||
|
||||
<div class="flex items-center justify-center mt-[20px] text-[#999] text-sm pb-8" v-if="copyright && loginType == 'admin'">
|
||||
<a :href="copyright.gov_url" v-if="copyright.gov_record" class="flex" target="_blank">
|
||||
<img src="@/app/assets/images/gov_icon.png" alt="" class="h-[20px] mr-1">
|
||||
<span class="mr-3">公安备案号:{{ copyright.gov_record }}</span>
|
||||
</a>
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" v-if="copyright.icp">
|
||||
<span class="mr-3">备案号:{{ copyright.icp }}</span>
|
||||
</a>
|
||||
<div class="flex items-center justify-center mt-[20px] pb-[20px] text-sm text-[#666]" v-if="copyright">
|
||||
<a :href="copyright.copyright_link" target="_blank">
|
||||
<span class="mr-3" v-if="copyright.copyright_desc">{{ copyright.copyright_desc }}</span>
|
||||
<span class="mr-3" v-if="copyright.company_name">{{ copyright.company_name }}</span>
|
||||
<span class="mr-3" v-if="copyright.copyright_desc">©{{ copyright.copyright_desc }}</span>
|
||||
</a>
|
||||
<a href="https://beian.miit.gov.cn/" v-if="copyright.icp" target="_blank">
|
||||
<span class="mr-3">{{ copyright.icp }}</span>
|
||||
</a>
|
||||
<a :href="copyright.gov_url" v-if="copyright.gov_record" target="_blank">
|
||||
<span class="mr-3">{{ copyright.gov_record }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -139,14 +108,15 @@ const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const copyright = ref(null)
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
getWebCopyright().then(({ data }) => {
|
||||
copyright.value = data
|
||||
})
|
||||
|
||||
route.redirectedFrom && (route.query.redirect = route.redirectedFrom.path)
|
||||
|
||||
const webSite = computed(() => useSystemStore().website)
|
||||
|
||||
|
||||
// 判断登录页面[平台或者站点]
|
||||
const loginType = ref(getAppType())
|
||||
|
||||
@ -164,10 +134,11 @@ const form = reactive({
|
||||
|
||||
// 获取登录配置信息
|
||||
const loginConfig = ref(null)
|
||||
const getLoginConfigFn = async (id: number = 0) => {
|
||||
const getLoginConfigFn = async () => {
|
||||
imgLoading.value = true
|
||||
const data = await (await getLoginConfig()).data
|
||||
loginConfig.value = data
|
||||
|
||||
imgLoading.value = false
|
||||
}
|
||||
getLoginConfigFn()
|
||||
@ -211,46 +182,35 @@ const loginFn = (data = {}) => {
|
||||
|
||||
<style lang="scss">
|
||||
.login-wrap {
|
||||
background-image: url("@/app/assets/images/login/login_index_bg.png");
|
||||
background-image: url("@/app/assets/images/login/login_index_bg.jpg");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.login-site-main {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.site-warp {
|
||||
background-image: url("@/app/assets/images/login/site_bg.png");
|
||||
background-color: #fff;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.site-login-wrap {
|
||||
// background-image: url('@/app/assets/images/back_login.jpg');
|
||||
// background-repeat: no-repeat;
|
||||
// background-size: cover;
|
||||
}
|
||||
|
||||
.input-with-select .el-input-group__prepend {
|
||||
background-color: var(--el-fill-color-blank);
|
||||
padding: 0 15px;
|
||||
background-image: url("@/app/assets/images/login/login_bg.jpg");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.login-main {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.login {
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
.login {
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.admin_name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
-o-text-overflow: ellipsis;
|
||||
.el-form-item__error {
|
||||
top : 45px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.formtwo .el-input__wrapper{
|
||||
// border: none !important;
|
||||
box-shadow: none !important;
|
||||
border-bottom: 1px solid #DCDEE0 !important;
|
||||
padding:10px 0 !important;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 750px) {
|
||||
@ -258,4 +218,4 @@ const loginFn = (data = {}) => {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
152
admin/src/app/views/marketing/sign-continue.vue
Normal file
@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<el-form :model="formData" :rules="formRules" class="page-form" ref="formRef">
|
||||
<el-form-item :label="t('continueSign')" prop="continue_sign">
|
||||
<el-input class="input-width" v-model.trim="formData.continue_sign" @keyup="filterNumber($event)" :maxlength="3" clearable />
|
||||
<span class="ml-[10px]">{{ t('day') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('continueSign')" >
|
||||
<div class="flex-1">
|
||||
<div v-for="(item,index) in gifts" :key="index" class="mb-[15px]">
|
||||
<component :is="item.component" v-model="formData[item.key]" ref="giftRefs" v-if="item.component" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('receiveLimit')" prop="receive_num">
|
||||
<div>
|
||||
<el-radio class="mb-[15px]" v-model="formData.receive_limit" :label="1" @change="radioChange($event, 1)">{{ t('noLimit') }}</el-radio>
|
||||
<div class="flex">
|
||||
<el-radio class="!mr-[15px]" v-model="formData.receive_limit" :label="2" @change="radioChange($event, 2)">{{ t('everyOneLimit') }}</el-radio>
|
||||
<el-input class="input-width" v-model.trim="formData.receive_num" :maxlength="5" clearable /><span class="ml-[10px]">{{ t('time') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, reactive, defineAsyncComponent, computed, watch } from 'vue'
|
||||
import { FormRules } from 'element-plus'
|
||||
import { getGiftDict } from '@/app/api/member'
|
||||
import { guid, filterNumber } from '@/utils/common'
|
||||
import Test from '@/utils/test'
|
||||
|
||||
const gifts = ref({})
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
const formData = ref({
|
||||
continue_sign: 0,
|
||||
continue_tag: guid(),
|
||||
receive_limit: 1,
|
||||
receive_num: 0,
|
||||
})
|
||||
|
||||
const value = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emits('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
const giftRefs = ref([])
|
||||
|
||||
watch(() => value.value, (nval, oval) => {
|
||||
if ((!oval || !Object.keys(oval).length) && Object.keys(nval).length) {
|
||||
formData.value = value.value
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => formData.value, () => {
|
||||
value.value = formData.value
|
||||
}, { deep: true })
|
||||
|
||||
const modules: any = import.meta.glob('@/**/*.vue')
|
||||
getGiftDict().then(({ data }) => {
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
data[key].component && (data[key].component = defineAsyncComponent(modules[data[key].component]))
|
||||
})
|
||||
gifts.value = data
|
||||
})
|
||||
|
||||
const formRef = ref(null)
|
||||
// 正则表达式
|
||||
const regExp = {
|
||||
required: /[\S]+/,
|
||||
number: /^\d{0,10}$/,
|
||||
digit: /^\d{0,10}(.?\d{0,2})$/,
|
||||
special: /^\d{0,10}(.?\d{0,3})$/
|
||||
}
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
continue_sign: [
|
||||
{ required: true, message: t('continueSignPlaceholder'), trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (isNaN(value) || !regExp.number.test(value)) {
|
||||
callback(t('continueSignFormatError'))
|
||||
} else if (value < 2 || value > 365) {
|
||||
callback(t('continueSignBerweenDays'))
|
||||
} else{
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
receive_num: [
|
||||
{ required: true, message: t('receiveNumPlaceholder'), trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: Function) => {
|
||||
if (formData.value.receive_limit == 2) {
|
||||
if (Test.empty(formData.value.receive_num)) {
|
||||
callback(t('receiveNumPlaceholder'))
|
||||
}
|
||||
if (isNaN(formData.value.receive_num) || !regExp.number.test(formData.value.receive_num)) {
|
||||
callback(t('receiveNumFormatError'))
|
||||
}
|
||||
if (formData.value.receive_num <= 0) {
|
||||
callback(t('receiveNumMustGreaterThanZeroTip'))
|
||||
}
|
||||
callback()
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const verify = async () => {
|
||||
let verify = true
|
||||
for (let i = 0; i < giftRefs.value.length; i++) {
|
||||
const item = giftRefs.value[i]
|
||||
!await item.verify() && (verify = false)
|
||||
}
|
||||
await formRef.value?.validate((valid) => {
|
||||
verify = valid
|
||||
})
|
||||
return verify
|
||||
}
|
||||
|
||||
const radioChange = (val, key) => {
|
||||
formData[key] = val
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
verify
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.input-width {
|
||||
width: 100px;
|
||||
}
|
||||
</style>
|
||||
71
admin/src/app/views/marketing/sign-day.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div v-for="(item,index) in gifts" :key="index" class="day-sign">
|
||||
<component :is="item.component" v-model="formData[item.key]" ref="giftRefs" v-if="item.component" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, defineAsyncComponent, computed, watch } from 'vue'
|
||||
import { getGiftDict } from '@/app/api/member'
|
||||
|
||||
const gifts = ref({})
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
const formData = ref({})
|
||||
const value = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emits('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
const giftRefs = ref([])
|
||||
|
||||
watch(() => value.value, (nval, oval) => {
|
||||
if ((!oval || !Object.keys(oval).length) && Object.keys(nval).length) {
|
||||
formData.value = value.value
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => formData.value, () => {
|
||||
value.value = formData.value
|
||||
}, { deep: true })
|
||||
|
||||
const modules: any = import.meta.glob('@/**/*.vue')
|
||||
getGiftDict().then(({ data }) => {
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
data[key].component && (data[key].component = defineAsyncComponent(modules[data[key].component]))
|
||||
})
|
||||
gifts.value = data
|
||||
})
|
||||
|
||||
const verify = async () => {
|
||||
let verify = true
|
||||
for (let i = 0; i < giftRefs.value.length; i++) {
|
||||
const item = giftRefs.value[i]
|
||||
!await item.verify() && (verify = false)
|
||||
}
|
||||
return verify
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
verify
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.day-sign:nth-child(1) :deep(.el-form-item__error) {
|
||||
left:48px;
|
||||
}
|
||||
.day-sign:nth-child(2) :deep(.el-form-item__error) {
|
||||
left:48px;
|
||||
}
|
||||
</style>
|
||||
@ -4,17 +4,17 @@
|
||||
|
||||
<el-form class="page-form" :model="formData" label-width="150px" ref="ruleFormRef" :rules="formRules" v-loading="loading">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('signRule') }}</h3>
|
||||
<h3 class="text-page-title">{{ t('signRule') }}</h3>
|
||||
|
||||
<el-form-item :label="t('isUse')">
|
||||
<el-switch v-model="formData.is_use" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('signPeriod')" v-if="formData.is_use">
|
||||
<el-input-number v-model="formData.sign_period" clearable class="input-width" controls-position="right" /><span class="ml-[10px]">天</span>
|
||||
<el-form-item :label="t('signPeriod')" prop="sign_period" v-if="formData.is_use">
|
||||
<el-input v-model.trim="formData.sign_period" @keyup="filterNumber($event)" maxlength="3" clearable class="input-width" /><span class="ml-[10px]">天</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('daySignAward')" prop="formData.day_award" v-if="formData.is_use">
|
||||
<el-form-item :label="t('daySignAward')" prop="day_award" v-if="formData.is_use">
|
||||
<div v-for="(item, index) in daySignAwardText" :key="index">
|
||||
<span v-if="item.is_use == '1'">{{ item.content }} </span>
|
||||
</div>
|
||||
@ -27,7 +27,7 @@
|
||||
<div class="form-tip">{{ t('daySignAwardTip') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('continueSignAward')" prop="formData.continue_award" v-if="formData.is_use">
|
||||
<el-form-item :label="t('continueSignAward')" prop="continue_award" v-if="formData.is_use">
|
||||
<div>
|
||||
<div class="form-tip">{{ t('continueSignAwardTipTop') }}</div>
|
||||
<div class="mt-[10px]">
|
||||
@ -68,9 +68,9 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('ruleExplain')" prop="formData.rule_explain" v-if="formData.is_use">
|
||||
<el-form-item :label="t('ruleExplain')" prop="rule_explain" v-if="formData.is_use">
|
||||
<div class="flex">
|
||||
<el-input v-model="formData.rule_explain" :placeholder="t('ruleExplainTip')" type="textarea" rows="5" class="textarea-width" clearable />
|
||||
<el-input v-model.trim="formData.rule_explain" :placeholder="t('ruleExplainTip')" type="textarea" maxlength="500" show-word-limit rows="5" class="textarea-width" clearable />
|
||||
<el-button class="ml-[20px]" type="primary" @click="defaultExplainEvent()" plain>{{ t('useDefaultExplain') }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@ -78,7 +78,7 @@
|
||||
</el-form>
|
||||
|
||||
<!-- 日签奖励 -->
|
||||
<el-dialog v-model="daySignDialog" :title="t('daySignTitle')" width="1200px" :destroy-on-close="true" v-if="formData.is_use">
|
||||
<el-dialog v-model="daySignDialog" :title="t('daySignTitle')" width="1000px" :destroy-on-close="true" v-if="formData.is_use">
|
||||
<sign-day ref="benefitsRef" v-model="formData.day_award" />
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@ -115,7 +115,9 @@ import { getSignConfig, setSignConfig, getMemberGiftsContent } from '@/app/api/m
|
||||
import signDay from '@/app/views/marketing/components/sign-day.vue'
|
||||
import signContinue from '@/app/views/marketing/components/sign-continue.vue'
|
||||
import { FormInstance, FormRules } from 'element-plus'
|
||||
import { filterNumber } from '@/utils/common'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
@ -129,18 +131,40 @@ const continue_award = ref({})
|
||||
let isEdit = false // 是否为编辑状态
|
||||
let editIndex = 0 // 连签奖励修改下标
|
||||
|
||||
// 正则表达式
|
||||
const regExp: any = {
|
||||
required: /[\S]+/,
|
||||
number: /^\d{0,10}$/,
|
||||
digit: /^\d{0,10}(.?\d{0,2})$/,
|
||||
special: /^\d{0,10}(.?\d{0,3})$/
|
||||
}
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
sign_period: [
|
||||
{ required: true, message: t('signPeriodTip'), trigger: 'blur' }
|
||||
]
|
||||
day_award: [
|
||||
{ required: true, message: t('daySignAwardPlaceholder'), trigger: 'change' }
|
||||
],
|
||||
sign_period:[{
|
||||
required: true,
|
||||
trigger: 'blur',
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (value === null || value === '') {
|
||||
callback(t('signPeriodTip'))
|
||||
}else if (isNaN(value) || !regExp.number.test(value)) {
|
||||
callback(t('signPeriodLimitTips'))
|
||||
}else if (value < 2 || value > 365) {
|
||||
callback(t('signPeriodMustZeroTips'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
/**
|
||||
* 签到奖励文本请求参数
|
||||
*/
|
||||
const contentData = reactive<Record<string, any>>({
|
||||
gifts: [],
|
||||
gifts: []
|
||||
})
|
||||
|
||||
/**
|
||||
@ -187,17 +211,16 @@ const setFormData = async () => {
|
||||
|
||||
if (formData.continue_award) {
|
||||
formData.continue_award.forEach((item: any, index: number) => {
|
||||
continueSignAwardTableData.data.push(cloneDeep(item))
|
||||
|
||||
continueSignAwardTableData.data.push(JSON.parse(JSON.stringify(item)))
|
||||
contentData.gifts = []
|
||||
|
||||
contentData.gifts = [];
|
||||
const val = cloneDeep(item)
|
||||
|
||||
const val = JSON.parse(JSON.stringify(item))
|
||||
|
||||
delete val['continue_sign'];
|
||||
delete val['continue_tag'];
|
||||
delete val['receive_limit'];
|
||||
delete val['receive_num'];
|
||||
delete val['continue_sign']
|
||||
delete val['continue_tag']
|
||||
delete val['receive_limit']
|
||||
delete val['receive_num']
|
||||
|
||||
contentData.gifts = val
|
||||
|
||||
@ -215,7 +238,7 @@ setFormData()
|
||||
const daySignAwardText = ref([])
|
||||
const setMemberBenefitsContent = async () => {
|
||||
const data = await (await getMemberGiftsContent(contentData)).data
|
||||
daySignAwardText.value = [];
|
||||
daySignAwardText.value = []
|
||||
Object.values(data).forEach((el: any) => {
|
||||
daySignAwardText.value.push(el)
|
||||
})
|
||||
@ -230,7 +253,7 @@ const setMemberBenefitsContent = async () => {
|
||||
*/
|
||||
const setMemberBenefitsContents = async (content: any, item: any, index: number = 0, tag = 0) => {
|
||||
const data = await (await getMemberGiftsContent(content)).data
|
||||
continueSignAwardText.value = [];
|
||||
continueSignAwardText.value = []
|
||||
Object.values(data).forEach((el: any) => {
|
||||
continueSignAwardText.value.push(el)
|
||||
})
|
||||
@ -242,17 +265,16 @@ const setMemberBenefitsContents = async (content: any, item: any, index: number
|
||||
|
||||
if (!isEdit) {
|
||||
if (tag == 0) {
|
||||
continueSignAwardTableData.data.splice(index, 1, JSON.parse(JSON.stringify(newArr)))
|
||||
continueSignAwardTableData.data.splice(index, 1, cloneDeep(newArr))
|
||||
} else {
|
||||
continueSignAwardTableData.data.push(JSON.parse(JSON.stringify(newArr)))
|
||||
continueSignAwardTableData.data.push(cloneDeep(newArr))
|
||||
}
|
||||
} else {
|
||||
continueSignAwardTableData.data.splice(editIndex, 1, JSON.parse(JSON.stringify(newArr)))
|
||||
continueSignAwardTableData.data.splice(editIndex, 1, cloneDeep(newArr))
|
||||
}
|
||||
|
||||
isEdit = false
|
||||
editIndex = 0
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -262,7 +284,7 @@ const onSave = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
const save = JSON.parse(JSON.stringify(formData))
|
||||
const save = cloneDeep(formData)
|
||||
setSignConfig(save).then(() => {
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
@ -289,6 +311,9 @@ const setDaySignAward = async () => {
|
||||
if (!formData.day_award.hasOwnProperty('balance') && !formData.day_award.hasOwnProperty('point') && formData.day_award.shop_coupon.is_use == 0) {
|
||||
formData.day_award = ''
|
||||
}
|
||||
if (formData.day_award.hasOwnProperty('balance') && formData.day_award.balance.is_use == 1) {
|
||||
formData.day_award.balance.money = Number(formData.day_award.balance.money)
|
||||
}
|
||||
contentData.gifts = formData.day_award
|
||||
|
||||
setMemberBenefitsContent()
|
||||
@ -314,8 +339,8 @@ const continueSignAwardSet = () => {
|
||||
* 修改连签奖励设置页
|
||||
*/
|
||||
const continueSignAwardModify = (flag: boolean, index: any) => {
|
||||
isEdit = flag;
|
||||
editIndex = index;
|
||||
isEdit = flag
|
||||
editIndex = index
|
||||
|
||||
continue_award.value = formData.continue_award[index]
|
||||
continueSignDialog.value = true
|
||||
@ -332,29 +357,30 @@ const setContinueSignAward = async () => {
|
||||
if (!continue_award.value.hasOwnProperty('balance') && !continue_award.value.hasOwnProperty('point') && continue_award.value.shop_coupon.is_use == 0) {
|
||||
continue_award.value = ''
|
||||
}
|
||||
|
||||
if (continue_award.value.hasOwnProperty('balance') && continue_award.value.balance.is_use == 1) {
|
||||
continue_award.value.balance.money = Number(continue_award.value.balance.money)
|
||||
}
|
||||
if (Object.keys(continue_award.value).length > 0) {
|
||||
const val = JSON.parse(JSON.stringify(continue_award.value))
|
||||
const val = cloneDeep(continue_award.value)
|
||||
|
||||
delete val['continue_sign'];
|
||||
delete val['continue_tag'];
|
||||
delete val['receive_limit'];
|
||||
delete val['receive_num'];
|
||||
delete val['continue_sign']
|
||||
delete val['continue_tag']
|
||||
delete val['receive_limit']
|
||||
delete val['receive_num']
|
||||
|
||||
contentData.gifts = val
|
||||
|
||||
let index = 0;
|
||||
let index = 0
|
||||
if (formData.continue_award.length > 0) {
|
||||
index = formData.continue_award.length - 1
|
||||
}
|
||||
|
||||
setMemberBenefitsContents(contentData, continue_award.value, 0, 1)
|
||||
|
||||
|
||||
if (!isEdit) {
|
||||
formData.continue_award.push(JSON.parse(JSON.stringify(continue_award.value)))
|
||||
formData.continue_award.push(cloneDeep(continue_award.value))
|
||||
} else {
|
||||
formData.continue_award.splice(editIndex, 1, JSON.parse(JSON.stringify(continue_award.value)))
|
||||
formData.continue_award.splice(editIndex, 1, cloneDeep(continue_award.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -371,7 +397,7 @@ const deleteContinueSignAwardEvent = (index: number) => {
|
||||
* 使用默认说明
|
||||
*/
|
||||
const defaultExplainEvent = () => {
|
||||
formData.rule_explain = t('ruleExplainDefault');
|
||||
formData.rule_explain = t('ruleExplainDefault')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
|
||||
<el-form :inline="true" :model="memberSignListTableData.searchParam" ref="searchFormRef">
|
||||
<el-form-item :label="t('memberInfo')" prop="keywords">
|
||||
<el-input v-model="memberSignListTableData.searchParam.keywords" class="w-[240px]" :placeholder="t('memberInfoPlaceholder')" />
|
||||
<el-input v-model.trim="memberSignListTableData.searchParam.keywords" class="w-[240px]" :placeholder="t('memberInfoPlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('createTime')" prop="create_time">
|
||||
<el-date-picker v-model="memberSignListTableData.searchParam.create_time" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" :start-placeholder="t('startDate')" :end-placeholder="t('endDate')" />
|
||||
@ -37,8 +37,8 @@
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center cursor-pointer" @click="toMember(row.member_id)">
|
||||
<img class="w-[50px] h-[50px] mr-[10px]" v-if="row.member.headimg" :src="img(row.member.headimg)" alt="">
|
||||
<img class="w-[50px] h-[50px] mr-[10px]" v-else src="@/app/assets/images/default_headimg.png" alt="">
|
||||
<div class="flex flex flex-col">
|
||||
<img class="w-[50px] h-[50px] mr-[10px] rounded-full" v-else src="@/app/assets/images/member_head.png" alt="">
|
||||
<div class="flex flex-col">
|
||||
<span>{{ row.member.nickname || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -7,10 +7,10 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('smsType')">
|
||||
<div class="input-width">
|
||||
<div class="input-width">
|
||||
<div v-if="formData.notice_type == 'sms'">{{ t('sms') }}</div>
|
||||
<div v-if="formData.notice_type == 'wechat'">{{ t('wechat') }}</div>
|
||||
<div v-if="formData.notice_type == 'weapp'">{{ t('weapp') }}</div>
|
||||
<div v-if="formData.notice_type == 'weapp'">{{ t('weapp') }}</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('updateWechat')" width="500px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
|
||||
<el-form-item :label="t('mchId')" prop="config.mch_id">
|
||||
<el-input v-model.trim="formData.config.mch_id" :placeholder="t('mchIdPlaceholder')" class="input-width" maxlength="32" show-word-limit clearable />
|
||||
@ -26,11 +26,52 @@
|
||||
<div class="form-tip">{{ t('mchPublicCertPathTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('wechatpayPublicCert')" prop="config.wechat_public_cert_path">
|
||||
<div class="input-width">
|
||||
<upload-file v-model="formData.config.wechat_public_cert_path" api="sys/document/wechat" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('wechatpayPublicCertId')" prop="config.wechat_public_cert_id">
|
||||
<div class="input-width">
|
||||
<el-input v-model.trim="formData.config.wechat_public_cert_id" placeholder="" class="input-width" show-word-limit clearable />
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('jsapiDir')" v-show="formData.channel == 'wechat' || formData.channel == 'weapp'">
|
||||
<el-input :model-value="wapDomain + '/'" placeholder="Please input" class="input-width" :readonly="true" :disabled="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(wapDomain + '/')">{{ t('copy') }}
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="form-tip !leading-normal">{{ t('jsapiDirTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('h5Domain')" v-show="formData.channel == 'h5'">
|
||||
<el-input :model-value="wapDomain.replace('http://', '').replace('https://', '')" placeholder="Please input" class="input-width" :readonly="true" :disabled="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(wapDomain.replace('http://', '').replace('https://', ''))">{{ t('copy') }}
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="form-tip !leading-normal">{{ t('h5DomainTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('nativeDomain')" v-show="formData.channel == 'pc'">
|
||||
<el-input :model-value="serviceDomain" placeholder="Please input" class="input-width" :readonly="true" :disabled="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(serviceDomain)">{{ t('copy') }}
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="form-tip !leading-normal">{{ t('nativeDomainTips') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button @click="cancel">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
@ -38,13 +79,24 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { FormInstance, ElMessage } from 'element-plus'
|
||||
import Test from '@/utils/test'
|
||||
import { getUrl } from '@/app/api/sys'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(true)
|
||||
const wapDomain = ref('')
|
||||
const serviceDomain = ref('')
|
||||
const initData = ref<any>(null)
|
||||
|
||||
getUrl().then((res: any) => {
|
||||
wapDomain.value = res.data.wap_domain
|
||||
serviceDomain.value = res.data.service_domain
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
@ -55,7 +107,9 @@ const initialFormData = {
|
||||
mch_id: '',
|
||||
mch_secret_key: '',
|
||||
mch_secret_cert: '',
|
||||
mch_public_cert_path: ''
|
||||
mch_public_cert_path: '',
|
||||
wechat_public_cert_path: '',
|
||||
wechat_public_cert_id: ''
|
||||
},
|
||||
channel: '',
|
||||
status: 0,
|
||||
@ -99,7 +153,20 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
})
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
Object.assign(formData, initialFormData)
|
||||
if (initData.value) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (initData.value[key] != undefined) formData[key] = initData.value[key]
|
||||
})
|
||||
formData.channel = initData.value.redio_key.split('_')[0]
|
||||
formData.status = Number(formData.status)
|
||||
}
|
||||
emit('complete', formData)
|
||||
showDialog.value = false
|
||||
}
|
||||
const setFormData = async (data: any = null) => {
|
||||
initData.value = cloneDeep(data)
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
if (data) {
|
||||
@ -118,6 +185,30 @@ const enableVerify = () => {
|
||||
return verify
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制
|
||||
*/
|
||||
const { copy, isSupported, copied } = useClipboard()
|
||||
const copyEvent = (text: string) => {
|
||||
if (!isSupported.value) {
|
||||
ElMessage({
|
||||
message: t('notSupportCopy'),
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
copy(text)
|
||||
}
|
||||
|
||||
watch(copied, () => {
|
||||
if (copied.value) {
|
||||
ElMessage({
|
||||
message: t('copySuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData,
|
||||
|
||||
@ -38,11 +38,11 @@ import { t } from '@/lang'
|
||||
import { setMap, getMap } from '@/app/api/sys'
|
||||
import { FormInstance } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const loading = ref(true)
|
||||
const formRef = ref<FormInstance>()
|
||||
const formData = reactive<Record<string, string>>({
|
||||
const formData = reactive({
|
||||
key: '',
|
||||
is_open: 1,
|
||||
is_open: 0,
|
||||
valid_time: 0
|
||||
})
|
||||
|
||||
@ -57,7 +57,7 @@ const formRules = computed(() => {
|
||||
callback(new Error(t('validTimePlaceholder')))
|
||||
} else if (isNaN(value) || !/^\d{0,10}$/.test(value)) {
|
||||
callback(new Error(t('validTimeFormatTips')))
|
||||
} else if (value < 0) {
|
||||
} else if (value < 5) {
|
||||
callback(new Error(t('validTimeNotZeroTips')))
|
||||
} else {
|
||||
callback()
|
||||
@ -69,8 +69,12 @@ const formRules = computed(() => {
|
||||
});
|
||||
|
||||
const setFormData = async () => {
|
||||
loading.value = true
|
||||
const service_data = await (await getMap()).data
|
||||
formData.key = service_data.key
|
||||
formData.is_open = service_data.is_open
|
||||
formData.valid_time = service_data.valid_time
|
||||
loading.value = false
|
||||
}
|
||||
setFormData()
|
||||
|
||||
|
||||
@ -10,12 +10,11 @@
|
||||
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
|
||||
<el-form :inline="true" :model="recordsTableData.searchParam" ref="searchFormRef">
|
||||
<el-form-item :label="t('searchReceiver')" prop="receiver">
|
||||
<el-input v-model="recordsTableData.searchParam.receiver" :placeholder="t('receiverPlaceholder')" />
|
||||
<el-input v-model.trim="recordsTableData.searchParam.receiver" :placeholder="t('receiverPlaceholder')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('noticeKey')" prop="key">
|
||||
<el-select v-model="recordsTableData.searchParam.key" clearable :placeholder="t('noticeKeyPlaceholder')" class="input-width">
|
||||
<el-option :label="t('selectPlaceholder')" value="" />
|
||||
<el-select v-model="recordsTableData.searchParam.key" clearable :placeholder="t('noticeKeyPlaceholder')" class="input-width" popper-class="notice">
|
||||
<el-option-group v-for="(group, gindex) in templateList" :key="gindex" :label="group.label">
|
||||
<el-option :label="item.name" :value="item.value" :disabled="item.disabled ?? false" v-for="(item, index) in group.list" :key="index" />
|
||||
</el-option-group>
|
||||
@ -28,7 +27,7 @@
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadNoticeLogList()">{{ t('search') }}</el-button>
|
||||
<el-button @click="searchFormRef?.resetFields()">{{ t('reset') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@ -76,7 +75,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getNoticeList, getNoticeLog } from '@/app/api/notice'
|
||||
import { getNoticeLog, getNoticeList } from '@/app/api/notice'
|
||||
import RecordsInfo from '@/app/views/setting/components/notice-records-info.vue'
|
||||
import { FormInstance } from 'element-plus'
|
||||
import { useRoute } from 'vue-router'
|
||||
@ -159,6 +158,12 @@ const loadNoticeLogList = (page: number = 1) => {
|
||||
}
|
||||
loadNoticeLogList()
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
loadNoticeLogList()
|
||||
}
|
||||
|
||||
const recordsDialog: Record<string, any> | null = ref(null)
|
||||
|
||||
/**
|
||||
@ -172,4 +177,9 @@ const infoEvent = (data: any) => {
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss">
|
||||
.notice .el-select-group__title{
|
||||
font-size: 14px;;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -113,7 +113,7 @@
|
||||
<div class="attachment-item mr-[10px] w-[120px]" v-for="(item, index) in attachment.data" :key="index">
|
||||
<div class="attachment-wrap w-full rounded cursor-pointer overflow-hidden relative flex items-center justify-center h-[120px]">
|
||||
<el-image :src="img(item.url)" fit="contain" v-if="type == 'image'" :preview-src-list="item.image_list"/>
|
||||
<video :src="img(item.url)" v-else-if="type == 'video'"></video>
|
||||
<video :src="img(item.url)" v-else-if="type == 'video'" @click="previewVideo(index)"></video>
|
||||
<icon :name="item.url" size="24px" v-else-if="type == 'icon'"></icon>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
@ -126,7 +126,7 @@
|
||||
</div>
|
||||
<div class="flex absolute top-0 left-0 right-0 bottom-0 items-center justify-center" v-else>
|
||||
<div class="flex flex-col items-center" v-if="!attachment.loading">
|
||||
<img src="@/app/assets/images/no_attachment.png" class="max-w-[130px] max-h-[130px] mb-[15px]">
|
||||
<img src="@/app/assets/images/no_attachment.png" class="max-w-[160px] max-h-[120px] mb-[15px]">
|
||||
<span class="text-[var(--el-text-color-secondary)] text-[14px]">{{type == 'icon' ? t('upload.iconEmpty') : t('upload.attachmentEmpty')}}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -197,11 +197,14 @@ import {
|
||||
getIconList
|
||||
} from '@/app/api/sys'
|
||||
import { debounce, img, getToken } from '@/utils/common'
|
||||
import { ElMessage, UploadFile, UploadFiles, ElMessageBox } from 'element-plus'
|
||||
import { ElMessage, UploadFile, UploadFiles, ElMessageBox, MessageParams } from 'element-plus'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const attachmentCategoryName = ref('')
|
||||
const operate = ref(false)
|
||||
|
||||
const repeat = ref(false)
|
||||
|
||||
const prop = defineProps({
|
||||
// 选择数量限制
|
||||
limit: {
|
||||
@ -303,6 +306,7 @@ const getAttachmentList = debounce((page: number = 1) => {
|
||||
attachment.loading = false
|
||||
})
|
||||
})
|
||||
|
||||
getAttachmentList()
|
||||
|
||||
watch(() => attachmentParam.cate_id, () => {
|
||||
@ -313,14 +317,18 @@ watch(() => attachmentParam.cate_id, () => {
|
||||
* 添加分组
|
||||
*/
|
||||
const addAttachmentCategory = (name: string) => {
|
||||
if (repeat.value) return
|
||||
repeat.value = true
|
||||
|
||||
addCategory({
|
||||
type: prop.type,
|
||||
name
|
||||
}).then(res => {
|
||||
repeat.value = false
|
||||
attachmentCategoryName.value = ''
|
||||
getAttachmentCategoryList(1)
|
||||
}).catch(() => {
|
||||
|
||||
repeat.value = false
|
||||
})
|
||||
}
|
||||
|
||||
@ -330,13 +338,16 @@ const addAttachmentCategory = (name: string) => {
|
||||
* @param index
|
||||
*/
|
||||
const updateAttachmentCategory = (name: string, index: number) => {
|
||||
if (repeat.value) return
|
||||
repeat.value = true
|
||||
updateCategory({
|
||||
id: attachmentCategory.data[index].id,
|
||||
name
|
||||
}).then(res => {
|
||||
repeat.value = false
|
||||
attachmentCategory.data[index].name = name
|
||||
}).catch(() => {
|
||||
|
||||
repeat.value = false
|
||||
})
|
||||
}
|
||||
|
||||
@ -344,6 +355,8 @@ const updateAttachmentCategory = (name: string, index: number) => {
|
||||
* 删除分组
|
||||
*/
|
||||
const deleteAttachmentCategory = (index: number) => {
|
||||
if (repeat.value) return
|
||||
repeat.value = true
|
||||
ElMessageBox.confirm(t('upload.deleteCategoryTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
@ -353,13 +366,15 @@ const deleteAttachmentCategory = (index: number) => {
|
||||
).then(() => {
|
||||
deleteCategory(attachmentCategory.data[index].id).then(() => {
|
||||
attachmentCategory.data.splice(index, 1)
|
||||
repeat.value = false
|
||||
}).catch(() => {
|
||||
repeat.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const uploadRef = ref<Record<string, any> | null>(null)
|
||||
|
||||
const time = ref<any>(null)
|
||||
// 上传文件
|
||||
const upload = computed(() => {
|
||||
const headers: Record<string, any> = {}
|
||||
@ -379,20 +394,45 @@ const upload = computed(() => {
|
||||
getAttachmentList()
|
||||
uploadRef.value?.handleRemove(uploadFile)
|
||||
} else {
|
||||
uploadFile.status = 'fail'
|
||||
ElMessage({ message: response.msg, type: 'error' })
|
||||
if(time.value==null) {
|
||||
time.value = setTimeout(() => {
|
||||
uploadFile.status = 'fail'
|
||||
showElMessage({ message: response.msg, type: 'error' })
|
||||
clearTimeout(time.value)
|
||||
time.value=null
|
||||
},500)
|
||||
|
||||
}else{
|
||||
clearTimeout(time.value)
|
||||
time.value=null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const messageCache = new Map()
|
||||
|
||||
const showElMessage = (options: MessageParams) => {
|
||||
const cacheKey = options.message
|
||||
const cachedMessage = messageCache.get(cacheKey)
|
||||
|
||||
if (!cachedMessage || Date.now() - cachedMessage.timestamp > 5000) { // 5秒内重复内容不再弹出,可自定义过期时间
|
||||
messageCache.set(cacheKey, { timestamp: Date.now() })
|
||||
ElMessage(options)
|
||||
}
|
||||
}
|
||||
|
||||
// 全选
|
||||
const selectAll = ref(false)
|
||||
watch(selectAll, () => {
|
||||
if (selectAll.value) {
|
||||
const keys = Object.keys(toRaw(selectedFile))
|
||||
attachment.data.forEach((item: Record<string, any>) => {
|
||||
if (!keys.includes(item.att_id)) selectedFile[item.att_id] = toRaw(item)
|
||||
if (!keys.includes(item.att_id)) {
|
||||
selectedFile[item.att_id] = toRaw(item)
|
||||
selectedFileIndex.push(item.att_id)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
clearSelected()
|
||||
@ -407,6 +447,7 @@ const clearSelected = () => {
|
||||
if (keys.length) {
|
||||
keys.forEach((key) => {
|
||||
delete selectedFile[key]
|
||||
selectedFileIndex.splice(selectedFileIndex.indexOf(key), 1)
|
||||
})
|
||||
selectAll.value = false
|
||||
}
|
||||
@ -425,7 +466,7 @@ const selectFile = (data: any) => {
|
||||
if (prop.limit == 1 && length == prop.limit) {
|
||||
delete selectedFile[keys[0]]
|
||||
selectedFileIndex.splice(selectedFileIndex.indexOf(keys[0]),1);
|
||||
} else if (length >= prop.limit) {
|
||||
} else if (prop.limit && length >= prop.limit) {
|
||||
ElMessage.info(t('upload.triggerUpperLimit'))
|
||||
return
|
||||
}
|
||||
|
||||
@ -1,37 +1,37 @@
|
||||
<template>
|
||||
<div :class="['layout-aside ease-in duration-200 flex h-full', { 'bright': !dark }]">
|
||||
<div class="flex flex-col h-full border-0 border-r-[1px] border-solid border-[var(--el-color-info-light-8)] box-border bg-[var(--el-color-info-light-8)]">
|
||||
<div class="w-full h-[64px] flex justify-center items-center w-[65px]flex-shrink-0">
|
||||
<div :class="['layout-aside ease-in duration-200 flex ', { 'bright': !dark }]">
|
||||
<div class="flex flex-col border-0 border-r-[1px] border-solid border-[var(--el-color-info-light-8)] box-border">
|
||||
<!-- <div class="w-full h-[64px] flex justify-center items-center w-[65px]flex-shrink-0">
|
||||
<div class="w-[40px] h-[40px] rounded-[50%] overflow-hidden">
|
||||
<el-image style="width: 100%; height: 100%" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[40px]"><img class="max-w-[40px]" src="@/app/assets/images/site_login_logo.png" alt="" object-fit="contain"></div>
|
||||
<div class="flex justify-center items-center w-full h-[40px]"><img class="max-w-[100px]" src="@/app/assets/images/site_login_logo.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
<el-scrollbar class="flex-1 w-[65px] one-menu">
|
||||
</div> -->
|
||||
<el-scrollbar class="w-[65px] one-menu">
|
||||
<div class="flex flex-col items-center">
|
||||
<template v-for="(item, index) in oneMenuData">
|
||||
<div v-if="item.meta.show" class="menu-item py-[10px] w-full box-border cursor-pointer" :class="{'is-active':oneMenuActive===item.original_name}" @click="router.push({ name: item.name })">
|
||||
<div class="w-[50px] h-full flex items-center justify-center mx-auto">
|
||||
<div v-if="item.meta.show" class="menu-item py-[10px] flex flex-col items-center justify-center w-full box-border cursor-pointer" :class="{'is-active':oneMenuActive===item.original_name}" @click="router.push({ name: item.name })">
|
||||
<div class="w-[35px] h-[35px] flex items-center justify-center mx-auto menu-icon" :class="{'is-active':oneMenuActive===item.original_name}">
|
||||
<template v-if="item.meta.icon">
|
||||
<el-image class="w-[25px] h-[25px] overflow-hidden" :src="item.meta.icon" fit="fill" v-if="isUrl(item.meta.icon)"/>
|
||||
<icon :name="item.meta.icon" size="25px" v-else />
|
||||
</template>
|
||||
<icon v-else :name="'iconfont iconshezhi1'" />
|
||||
</div>
|
||||
<div class="text-center text-[13px] mt-[5px]">{{ item.meta.short_title || item.meta.title }}</div>
|
||||
<div class="text-center text-[13px] mt-[3px]">{{ item.meta.short_title || item.meta.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-col two-menu w-[140px] h-[100vh]" v-if="twoMenuData.length">
|
||||
<div class="w-[140px] h-[64px] flex items-center justify-center text-[16px]">{{ route.matched[1].meta.title }}</div>
|
||||
<div class="flex flex-col two-menu w-[185px] " v-if="twoMenuData.length">
|
||||
<!-- <div class="w-[175px] h-[64px] flex items-center justify-center text-[16px]">{{ route.matched[1].meta.title }}</div> -->
|
||||
<el-scrollbar class="flex-1">
|
||||
<el-menu :default-active="route.name" :router="true" class="aside-menu " :collapse="systemStore.menuIsCollapse">
|
||||
<el-menu :default-active="route.name" :default-openeds="defaultOpeneds" :router="true" class="aside-menu" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in twoMenuData" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
@ -62,6 +62,7 @@ const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
})
|
||||
const twoMenuData = ref<Record<string, any>[]>([])
|
||||
const defaultOpeneds = ref<string[]>([]) // 默认打开的菜单项路径数组
|
||||
|
||||
const oneMenuData = ref<Record<string, any>[]>([])
|
||||
routers.forEach(item => {
|
||||
@ -76,6 +77,7 @@ const oneMenuActive = ref(oneMenuData.value[0].name)
|
||||
watch(route, () => {
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
oneMenuActive.value = route.matched[1].name == ADMIN_ROUTE.children[0].name ? route.matched[2].name : route.matched[1].name
|
||||
defaultOpeneds.value = twoMenuData.value.map(item => item.name)
|
||||
}, { immediate: true })
|
||||
|
||||
</script>
|
||||
@ -83,12 +85,25 @@ watch(route, () => {
|
||||
<style lang="scss">
|
||||
.one-menu{
|
||||
.menu-item{
|
||||
.menu-icon {
|
||||
background-color: transparent; /* 默认无背景色 */
|
||||
color: #8F9ABF;
|
||||
}
|
||||
|
||||
.menu-icon.is-active {
|
||||
background-color: var(--el-color-primary); /* 选中时背景色 */
|
||||
color: white; /* 选中时图标颜色变白 */
|
||||
border-radius: 4px; /* 可选:使图标背景为圆形 */
|
||||
}
|
||||
|
||||
&:hover{
|
||||
color:var(--el-color-primary);
|
||||
}
|
||||
&.is-active{
|
||||
background-color: var(--el-color-primary) !important;
|
||||
color: #fff !important;
|
||||
// background-color: var(--el-color-primary) !important;
|
||||
// color: #fff !important;
|
||||
border: none;
|
||||
color:var(--el-color-primary);
|
||||
}
|
||||
span{
|
||||
font-size: 14px;
|
||||
@ -104,12 +119,12 @@ watch(route, () => {
|
||||
}
|
||||
.two-menu{
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
width: 140px;
|
||||
width: 185px;
|
||||
border: 0;
|
||||
padding-top: 10px;
|
||||
padding-top: 15px;
|
||||
.el-menu-item{
|
||||
height: 36px;
|
||||
margin: 0 8px 4px;
|
||||
margin: 4px 10px;
|
||||
padding: 0 8px !important;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
@ -117,17 +132,21 @@ watch(route, () => {
|
||||
font-size: 14px;
|
||||
}
|
||||
&.is-active{
|
||||
background-color: var(--el-color-info-light-8) !important;
|
||||
// background-color: var(--el-color-info-light-8) !important;
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
&:hover{
|
||||
background-color: var(--el-color-info-light-8) !important;
|
||||
// background-color: var(--el-color-info-light-8) !important;
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.el-sub-menu{
|
||||
margin-bottom: 8px;
|
||||
width: 185px;
|
||||
margin: 4px 0px;
|
||||
// margin-bottom: 8px;
|
||||
.el-sub-menu__title{
|
||||
margin: 0 8px 4px;
|
||||
margin: 0px 10px;
|
||||
height: 36px;
|
||||
padding-left: 8px;
|
||||
border-radius: 2px;
|
||||
@ -138,7 +157,8 @@ watch(route, () => {
|
||||
font-size: 14px;
|
||||
}
|
||||
&:hover{
|
||||
background-color: var(--el-color-info-light-8) !important;
|
||||
// background-color: var(--el-color-info-light-8) !important;
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.el-icon.el-sub-menu__icon-arrow{
|
||||
@ -146,7 +166,7 @@ watch(route, () => {
|
||||
}
|
||||
}
|
||||
.el-menu-item{
|
||||
padding-left: 20px !important;
|
||||
padding-left: 25px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -175,16 +195,16 @@ watch(route, () => {
|
||||
.layout-aside .el-scrollbar__wrap--hidden-default, .layout-aside .el-scrollbar{
|
||||
overflow: inherit !important;
|
||||
}
|
||||
.layout-aside .menu-item.is-active{
|
||||
position: relative;
|
||||
&:after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: var(--el-color-primary);
|
||||
right: -1px;
|
||||
}
|
||||
}
|
||||
// .layout-aside .menu-item.is-active{
|
||||
// position: relative;
|
||||
// &:after{
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// width: 1px;
|
||||
// background: var(--el-color-primary);
|
||||
// right: -1px;
|
||||
// }
|
||||
// }
|
||||
</style>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<div v-if="meta.icon && routes.meta.class == 1" class="w-[16px] h-[16px] relative flex items-center">
|
||||
<div v-if="meta.icon " class="w-[13px] h-[13px] mr-[10rpx] relative flex justify-center items-center">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span :class="['ml-[10px]', {'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}]">{{ meta.title }}</span>
|
||||
@ -11,6 +11,9 @@
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else :index="String(routes.name)" :route="routes.path">
|
||||
<template #title>
|
||||
<div v-if="meta.icon " class="w-[13px] h-[13px] mr-[10rpx] relative flex justify-center items-center">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
@ -33,6 +36,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const meta = computed(() => props.routes.meta)
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@ -1,27 +1,42 @@
|
||||
<template>
|
||||
<el-container class="h-[64px] layout-admin flex items-center justify-between px-[15px]" >
|
||||
<el-container class="h-[64px] w-full layout-admin flex items-center justify-between px-[15px]" >
|
||||
<!-- :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" -->
|
||||
<div class="left-panel flex items-center text-[14px] leading-[1]">
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element Refresh" />
|
||||
<div class="flex items-center">
|
||||
<div class="w-[120px] flex justify-center items-center flex-shrink-0">
|
||||
<div class="w-[120px] h-[40px] overflow-hidden">
|
||||
<el-image style="width: 100%; height: 100%" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-full"><img class="max-w-[120px]" src="@/app/assets/images/logo.default.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<div class="left-panel flex items-center text-[14px] leading-[1]">
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
|
||||
<div>
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
@ -60,16 +75,21 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { img, isUrl } from '@/utils/common'
|
||||
|
||||
import { t } from '@/lang'
|
||||
import storage from '@/utils/storage'
|
||||
import userInfo from './user-info.vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
})
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
const comparisonToken = ref('')
|
||||
|
||||
@ -1,31 +1,31 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
|
||||
<el-container>
|
||||
<!-- 顶部 -->
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<!-- 顶部 end -->
|
||||
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
<el-container :style="{height:'calc(100vh - 64px)'}">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<el-aside class="layout-aside dark w-auto">
|
||||
<side class="hidden-xs-only slide" />
|
||||
<el-scrollbar>
|
||||
<side class="hidden-xs-only slide" />
|
||||
</el-scrollbar>
|
||||
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
@ -29,15 +32,15 @@ watch(route, () => {
|
||||
//background-color: var(--side-dark-color, var(--el-bg-color));
|
||||
|
||||
&.bright {
|
||||
background-color: #F5F7F9;
|
||||
// background-color: #F5F7F9;
|
||||
|
||||
li {
|
||||
background-color: #F5F7F9;
|
||||
// background-color: #F5F7F9;
|
||||
|
||||
&.is-active:not(.is-opened) {
|
||||
position: relative;
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
// background-color: #fff;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
|
||||
@ -1,27 +1,22 @@
|
||||
<template>
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="meta.type == 0 && routes.children" :index="String(routes.name)">
|
||||
<el-sub-menu v-if="meta.type == 0 && routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
<icon v-if="meta.icon && systemStore.menuIsCollapse || props.level != 1" :name="meta.icon" :size="(meta.icon && systemStore.menuIsCollapse && props.level == 1) ? '20px' : '16px'" class="absolute" />
|
||||
<span class="text-[14px]" :class="{ 'font-bold': props.level === 1 , ' ml-[20px]': (meta.icon && props.level != 1) }">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" :level="props.level + 1" />
|
||||
</el-sub-menu>
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })">
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" >
|
||||
<icon v-if="meta.icon && systemStore.menuIsCollapse || props.level != 1" :name="meta.icon" :size="(meta.icon && systemStore.menuIsCollapse && props.level == 1) ? '20px' : '16px'" class="absolute" />
|
||||
<template #title>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
<span class="text-[14px]" :class="{ 'font-bold': props.level === 1 , ' ml-[20px]': (meta.icon && props.level != 1) }">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
@ -29,7 +24,6 @@ import { img } from '@/utils/common'
|
||||
import menuItem from './menu-item.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
@ -82,7 +76,21 @@ watch(route, () => {
|
||||
<style lang="scss">
|
||||
.el-sub-menu{
|
||||
.el-icon{
|
||||
width: auto;
|
||||
// width: auto;
|
||||
}
|
||||
}
|
||||
.el-menu {
|
||||
.el-sub-menu__title,
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
// background-color: #F1F5FF !important;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon.el-sub-menu__icon-arrow{
|
||||
font-size: 15px;
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,22 +1,8 @@
|
||||
<template>
|
||||
<el-container class="h-screen flex flex-col">
|
||||
<el-container class="flex flex-col">
|
||||
<el-main class="menu-wrap">
|
||||
<el-header class="logo-wrap flex items-center justify-center h-[64px] w-[var(--aside-width)]">
|
||||
<div class="flex justify-center items-center h-[64px] w-full px-[10px]" v-if="Object.keys(website).length">
|
||||
<el-image :src="img(website.icon)" class="w-[44px] h-[44px] rounded-[50%]" @error="website.icon = img('static/resource/images/niucloud_icon.jpg')"></el-image>
|
||||
<div class="flex-1 w-0 overflow-text truncate ml-[10px] text-white" v-if="!systemStore.menuIsCollapse">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="website.site_name"
|
||||
placement="top"
|
||||
>
|
||||
{{ website.site_name }}
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-scrollbar class="menu-scrollbar flex-1 h-0">
|
||||
<el-menu :default-active="route.name" :router="true" :unique-opened="false" :collapse="systemStore.menuIsCollapse" background-color="#1f2531" text-color="#fff" active-text-color="#fff">
|
||||
<el-menu :default-active="route.name" :default-openeds="defaultOpeneds" :router="true" :unique-opened="false" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in menuData" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
@ -26,7 +12,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed ,onMounted} from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
@ -45,7 +31,12 @@ const addonRouters: Record<string, any> = {}
|
||||
const website = computed(() => {
|
||||
return systemStore.website
|
||||
})
|
||||
// 默认打开的菜单项路径数组
|
||||
const defaultOpeneds = ref<string[]>([])
|
||||
// 在组件挂载后,生成默认打开的菜单项路径数组
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
@ -81,11 +72,14 @@ if (siteInfo?.apps.length > 1) {
|
||||
})
|
||||
menuData.value.unshift(...routers)
|
||||
}
|
||||
|
||||
defaultOpeneds.value = menuData.value.map(item => item.name)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.logo-wrap {
|
||||
background: #1f2531;
|
||||
// background: #1f2531;
|
||||
background: #fff;
|
||||
transition: transform getCssVar('transition-duration');
|
||||
}
|
||||
:root{
|
||||
@ -93,7 +87,8 @@ if (siteInfo?.apps.length > 1) {
|
||||
}
|
||||
.menu-wrap {
|
||||
padding: 0!important;
|
||||
background: #1f2531;
|
||||
// background: #1f2531;
|
||||
// background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -113,13 +108,24 @@ if (siteInfo?.apps.length > 1) {
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
background: var(--el-color-primary);
|
||||
background: var(--el-color-primary) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
&.el-menu--inline {
|
||||
background: #282e3a;
|
||||
// background: #282e3a;
|
||||
// background: #fff;
|
||||
}
|
||||
.el-menu-item .el-menu-tooltip__trigger{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
&.el-menu--collapse .el-sub-menu__title{
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,31 +1,46 @@
|
||||
<template>
|
||||
<el-container class="h-[64px] layout-admin flex items-center justify-between px-[15px]" >
|
||||
<el-container class="h-[64px] layout-admin flex items-center justify-between px-[15px]" >
|
||||
<!-- :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" -->
|
||||
<div class="left-panel flex items-center text-[14px] leading-[1]">
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element Fold" v-else />
|
||||
<div class="flex items-center">
|
||||
<div class="w-[120px] flex justify-center items-center flex-shrink-0">
|
||||
<div class="w-[120px] h-[40px] overflow-hidden">
|
||||
<el-image class="w-full h-full" :src="img(website.icon)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-full"><img class="max-w-[120px]" src="@/app/assets/images/logo.default.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<div class="left-panel flex items-center text-[14px] leading-[1]">
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element Fold" v-else />
|
||||
</div>
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
<div>
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
@ -66,6 +81,8 @@ import useUserStore from '@/stores/modules/user'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
import { img, isUrl } from '@/utils/common'
|
||||
|
||||
import storage from '@/utils/storage'
|
||||
import userInfo from './user-info.vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
@ -75,6 +92,9 @@ const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const systemStore = useSystemStore()
|
||||
const website = computed(() => {
|
||||
return systemStore.website
|
||||
})
|
||||
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
|
||||
@ -1,31 +1,31 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
|
||||
<el-container>
|
||||
<!-- 顶部 -->
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<!-- 顶部 end -->
|
||||
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
<el-container :style="{height:'calc(100vh - 64px)'}">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@ -9,12 +9,7 @@
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-if="meta.addon && meta.parent_route && meta.parent_route.addon == ''">
|
||||
<template #title>
|
||||
<el-tooltip placement="right" effect="light">
|
||||
<template #content>
|
||||
该功能仅限{{ addons[meta.addon].title }}使用
|
||||
</template>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
</el-tooltip>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-else>
|
||||
|
||||
@ -69,10 +69,12 @@ const logoUrl = computed(() => {
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
if (item.meta.attr == '') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
oneMenuData.value.push(item)
|
||||
}
|
||||
oneMenuData.value.push(item)
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length <= 1 && siteInfo?.apps[0].key == item.meta.addon) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
@ -105,23 +107,28 @@ if (siteInfo?.apps.length > 1) {
|
||||
const oneMenuActive = ref(route.matched[1].name)
|
||||
|
||||
watch(route, () => {
|
||||
// 多应用
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
twoMenuData.value = route.matched[1].children
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
if (route.meta.attr != '') {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
} else {
|
||||
// 单应用
|
||||
const oneMenu = route.matched[1]
|
||||
if (oneMenu.meta.addon == '') {
|
||||
// 多应用
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
twoMenuData.value = route.matched[1].children
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
} else {
|
||||
if (oneMenu.meta.addon == siteInfo?.apps[0].key) {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
twoMenuData.value = route.matched[2].children ?? []
|
||||
} else {
|
||||
// 单应用
|
||||
const oneMenu = route.matched[1]
|
||||
if (oneMenu.meta.addon == '') {
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
} else {
|
||||
if (oneMenu.meta.addon == siteInfo?.apps[0].key) {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
twoMenuData.value = route.matched[2].children ?? []
|
||||
} else {
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,15 +17,10 @@
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-if="meta.addon && meta.parent_route && meta.parent_route.addon == ''">
|
||||
<template #title>
|
||||
<el-tooltip placement="right" effect="light">
|
||||
<template #content>
|
||||
该功能仅限{{ addons[meta.addon].title }}使用
|
||||
</template>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</el-tooltip>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-else>
|
||||
@ -84,6 +79,9 @@ routers.forEach(item => {
|
||||
if (item.meta.addon) {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
if (item.meta.attr) {
|
||||
addonRouters[item.meta.attr] = item
|
||||
}
|
||||
})
|
||||
|
||||
const addonsMenus = ref(null)
|
||||
@ -93,6 +91,8 @@ watch(route, () => {
|
||||
|
||||
if (systemAddonKeys.value.includes(route.meta.addon) && addonRouters[route.meta.addon]) {
|
||||
addonsMenus.value = addonRouters[route.meta.addon]
|
||||
} else if (route.meta.attr && addonRouters[route.meta.attr]) {
|
||||
addonsMenus.value = addonRouters[route.meta.attr]
|
||||
} else {
|
||||
addonsMenus.value = null
|
||||
}
|
||||
|
||||
@ -47,10 +47,12 @@ const logoUrl = computed(() => {
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
if (item.meta.attr == '') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
menuData.value.push(item)
|
||||
}
|
||||
menuData.value.push(item)
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length == 1 && siteInfo?.apps[0].key == item.meta.addon) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
|
||||
@ -17,15 +17,10 @@
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-if="meta.addon && meta.parent_route && meta.parent_route.addon == ''">
|
||||
<template #title>
|
||||
<el-tooltip placement="right" effect="light">
|
||||
<template #content>
|
||||
该功能仅限{{ addons[meta.addon].title }}使用
|
||||
</template>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</el-tooltip>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-else>
|
||||
@ -84,6 +79,9 @@ routers.forEach(item => {
|
||||
if (item.meta.addon) {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
if (item.meta.attr) {
|
||||
addonRouters[item.meta.attr] = item
|
||||
}
|
||||
})
|
||||
|
||||
const addonsMenus = ref(null)
|
||||
@ -93,6 +91,8 @@ watch(route, () => {
|
||||
|
||||
if (systemAddonKeys.value.includes(route.meta.addon) && addonRouters[route.meta.addon]) {
|
||||
addonsMenus.value = addonRouters[route.meta.addon]
|
||||
} else if (route.meta.attr && addonRouters[route.meta.attr]) {
|
||||
addonsMenus.value = addonRouters[route.meta.attr]
|
||||
} else {
|
||||
addonsMenus.value = null
|
||||
}
|
||||
|
||||