Merge branch 'master' of https://gitea.zianxin.com/zax/biz-mobile-react-cloud
This commit is contained in:
commit
f075a42ff1
81 files changed
+6614
-1369
No files matched your search
@@ -0,0 +1,51 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 提交意见反馈
|
||||
*/
|
||||
submitFeedback: (data: {
|
||||
title: string;
|
||||
description: string;
|
||||
picUrl?: string;
|
||||
type: string;
|
||||
contact?: string;
|
||||
}) =>
|
||||
request({
|
||||
url: '/integrated/feedback/app/save',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取我的反馈列表
|
||||
*/
|
||||
getFeedbackList: (params: { current: number; size: number }) =>
|
||||
request({
|
||||
url: '/integrated/feedback/app/page',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取反馈详情
|
||||
*/
|
||||
getFeedbackDetail: (id: string) =>
|
||||
request({
|
||||
url: '/integrated/feedback/app/getDetails',
|
||||
method: 'GET',
|
||||
params: { id },
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取反馈类型字典
|
||||
*/
|
||||
getFeedbackType: () =>
|
||||
request({
|
||||
url: '/integrated/feedback/app/getFeedbackType',
|
||||
method: 'GET',
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -42,4 +42,6 @@ export { default as invitationApi } from "./invitation"
|
||||
|
||||
export { default as totalApi } from "./total"
|
||||
|
||||
export { default as userCarApi } from "./userCar"
|
||||
export { default as userCarApi } from "./userCar"
|
||||
|
||||
export { default as prosperityShopApi } from "./prosperityShop"
|
||||
@@ -16,7 +16,7 @@ export default {
|
||||
data,
|
||||
}),
|
||||
|
||||
// 获取访客邀约列表
|
||||
// 获取访客邀约记录列表
|
||||
// type为1:主动申请 type为2:邀约 type为3:后台创建
|
||||
getInvitationRecord: (params: { current: number; size: number; checkStatus?: string; type?: number }): Promise<any> =>
|
||||
request({
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 提交维修申请
|
||||
*/
|
||||
submitRepair: (data: {
|
||||
title: string;
|
||||
picUrl?: string;
|
||||
description: string;
|
||||
dateFound: string;
|
||||
location: string;
|
||||
urgency: string;
|
||||
maintainType: number;
|
||||
}) =>
|
||||
request({
|
||||
url: '/integrated/maintenanceOrder/app/apply',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取我的维修记录列表
|
||||
*/
|
||||
getRepairList: (params: { current: number; size: number }) =>
|
||||
request({
|
||||
url: '/integrated/maintenanceOrder/app/my',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取维修详情
|
||||
*/
|
||||
getRepairDetail: (id: string | number) =>
|
||||
request({
|
||||
url: `/integrated/maintenanceOrder/app/my/${id}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取维修类型选项
|
||||
*/
|
||||
getFeedbackType: () =>
|
||||
request({
|
||||
url: '/integrated/maintenanceOrder/app/getFeedbackType',
|
||||
method: 'GET',
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
+11
-147
@@ -31,6 +31,8 @@ export default {
|
||||
title: string;
|
||||
description: string;
|
||||
attendees: string[];
|
||||
need_service?: string;
|
||||
desk_layout?: string;
|
||||
}) =>
|
||||
request({
|
||||
url: `/integrated/meetingRoom/${roomId}/book`,
|
||||
@@ -57,154 +59,16 @@ export default {
|
||||
method: 'GET',
|
||||
}),
|
||||
|
||||
// ========== 意见反馈相关接口 ==========
|
||||
/**
|
||||
* 提交意见反馈
|
||||
*/
|
||||
submitFeedback: (data: {
|
||||
title: string;
|
||||
description: string;
|
||||
picUrl?: string;
|
||||
type: string;
|
||||
contact?: string;
|
||||
}) =>
|
||||
request({
|
||||
url: '/integrated/feedback/app/save',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取我的反馈列表
|
||||
*/
|
||||
getFeedbackList: (params: { current: number; size: number }) =>
|
||||
request({
|
||||
url: '/integrated/feedback/app/page',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
/**
|
||||
* 获取桌子拜访形态
|
||||
*/
|
||||
getMeetingDeskLayout: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/meetingDeskLayout/app/list`,
|
||||
method: 'GET',
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取反馈详情
|
||||
*/
|
||||
getFeedbackDetail: (id: string) =>
|
||||
request({
|
||||
url: '/integrated/feedback/app/getDetails',
|
||||
method: 'GET',
|
||||
params: { id },
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取反馈类型字典
|
||||
*/
|
||||
getFeedbackType: () =>
|
||||
request({
|
||||
url: '/integrated/feedback/app/getFeedbackType',
|
||||
method: 'GET',
|
||||
}),
|
||||
|
||||
// ========== 维修上报相关接口 ==========
|
||||
/**
|
||||
* 提交维修申请
|
||||
*/
|
||||
submitRepair: (data: {
|
||||
title: string;
|
||||
picUrl?: string;
|
||||
description: string;
|
||||
dateFound: string;
|
||||
location: string;
|
||||
urgency: string;
|
||||
maintainType: number;
|
||||
}) =>
|
||||
request({
|
||||
url: '/integrated/maintenanceOrder/app/apply',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取我的维修记录列表
|
||||
*/
|
||||
getRepairList: (params: { current: number; size: number }) =>
|
||||
request({
|
||||
url: '/integrated/maintenanceOrder/app/my',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取维修详情
|
||||
*/
|
||||
getRepairDetail: (id: string | number) =>
|
||||
request({
|
||||
url: `/integrated/maintenanceOrder/app/my/${id}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
|
||||
// ========== 通知公告相关接口 ==========
|
||||
/**
|
||||
* 获取公告列表
|
||||
*/
|
||||
getNoticeList: (params: { current: number; size: number; type?: string }) =>
|
||||
request({
|
||||
url: '/integrated/notice/list',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取公告详情
|
||||
*/
|
||||
getNoticeDetail: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/notice/detail/${id}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
|
||||
/**
|
||||
* 标记公告已读
|
||||
*/
|
||||
markNoticeRead: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/notice/read/${id}`,
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// ========== 访客管理相关接口 ==========
|
||||
/**
|
||||
* 提交访客申请
|
||||
*/
|
||||
submitVisitor: (data: {
|
||||
visitorName: string;
|
||||
visitorPhone: string;
|
||||
purpose: string;
|
||||
visitDate: string;
|
||||
visitTime: string;
|
||||
hostName: string;
|
||||
remark?: string;
|
||||
}) =>
|
||||
request({
|
||||
url: '/integrated/visitor/submit',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取访客记录列表
|
||||
*/
|
||||
getVisitorList: (params: { current: number; size: number }) =>
|
||||
request({
|
||||
url: '/integrated/visitor/my-list',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取访客详情
|
||||
*/
|
||||
getVisitorDetail: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/visitor/detail/${id}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
}
|
||||
@@ -20,4 +20,33 @@ export default {
|
||||
url: `/integrated/notice/app/view/${id}`,
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// ========== 通知公告相关接口 ==========
|
||||
/**
|
||||
* 获取公告列表
|
||||
*/
|
||||
getNoticeList: (params: { current: number; size: number; type?: string }) =>
|
||||
request({
|
||||
url: '/integrated/notice/list',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取公告详情
|
||||
*/
|
||||
getNoticeDetailById: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/notice/detail/${id}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
|
||||
/**
|
||||
* 标记公告已读
|
||||
*/
|
||||
markNoticeRead: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/notice/read/${id}`,
|
||||
method: 'POST',
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
// 获取超市列表
|
||||
getShopList: (params: { productLimit: number }): Promise<any> => {
|
||||
return request({
|
||||
url: "/supermarket/app/mall/merchant/list",
|
||||
method: "GET",
|
||||
params: { productLimit: 5 },
|
||||
})
|
||||
},
|
||||
// 获取商品分类列表
|
||||
getGoodsCategory: (params?: any): Promise<any> => {
|
||||
return request({
|
||||
url: "/supermarket/app/mall/merchant/category/list",
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
// 获取超市详情
|
||||
getShopDetail: (id: string): Promise<any> => {
|
||||
return request({
|
||||
url: `/supermarket/app/mall/merchant/${id}`,
|
||||
method: "GET",
|
||||
})
|
||||
},
|
||||
// 获取超市商品分类列表树
|
||||
getGoodsCategoryTree: (params?: { merchantId: string }): Promise<any> => {
|
||||
return request({
|
||||
url: `/supermarket/app/mall/category/tree`,
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
// 获取商品列表
|
||||
getGoodsList: (params: any): Promise<any> => {
|
||||
return request({
|
||||
url: `/supermarket/app/mall/product/page`,
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
// 获取商品详情
|
||||
getGoodsDetail: (id: string): Promise<any> => {
|
||||
return request({
|
||||
url: `/supermarket/app/mall/product/${id}`,
|
||||
method: "GET",
|
||||
})
|
||||
},
|
||||
// 共富商城商品管理获取自己商户的商品列表
|
||||
getManageGoodsList: (params: any): Promise<any> => {
|
||||
return request({
|
||||
url: `/supermarket/app/mall/merchant/product/list`,
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
// 删除商品
|
||||
deleteGoods: (id: string): Promise<any> => {
|
||||
return request({
|
||||
url: `/supermarket/app/mall/merchant/product/${id}`,
|
||||
method: "DELETE",
|
||||
})
|
||||
},
|
||||
// 新增商品
|
||||
addGoods: (data: any): Promise<any> => {
|
||||
return request({
|
||||
url: `/supermarket/app/mall/merchant/product/create`,
|
||||
method: "POST",
|
||||
data,
|
||||
})
|
||||
},
|
||||
// 修改商品
|
||||
editGoods: (data: any): Promise<any> => {
|
||||
return request({
|
||||
url: `/supermarket/app/mall/merchant/product/update`,
|
||||
method: "PUT",
|
||||
data,
|
||||
})
|
||||
},
|
||||
// 删除商品分类
|
||||
delCategary: (id: any): Promise<any> => {
|
||||
return request({
|
||||
url: `/supermarket/app/mall/merchant/category/${id}`,
|
||||
method: "DELETE",
|
||||
})
|
||||
},
|
||||
// 新增商品分类
|
||||
addCategary: (data: any): Promise<any> => {
|
||||
return request({
|
||||
url: `/supermarket/app/mall/merchant/category/create`,
|
||||
method: "POST",
|
||||
data,
|
||||
})
|
||||
},
|
||||
// 编辑商品分类
|
||||
editCategary: (data: any): Promise<any> => {
|
||||
return request({
|
||||
url: `/supermarket/app/mall/merchant/category/update`,
|
||||
method: "PUT",
|
||||
data,
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 查询任务详情
|
||||
*/
|
||||
queryTask: (taskId: string, view: boolean = false) =>
|
||||
request({
|
||||
url: "/task/task/queryTask",
|
||||
method: "GET",
|
||||
params: { taskId, view },
|
||||
}),
|
||||
|
||||
/**
|
||||
* 格式化开始节点显示
|
||||
*/
|
||||
formatStartNodeShow: (data: {
|
||||
flowId: string;
|
||||
processInstanceId: string;
|
||||
paramMap: any;
|
||||
taskId: string;
|
||||
}) =>
|
||||
request({
|
||||
url: "/task/process-instance/formatStartNodeShow",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 完成任务(审批)
|
||||
*/
|
||||
completeTask: (data: {
|
||||
taskId: string;
|
||||
paramMap: any;
|
||||
taskLocalParamMap: {
|
||||
approveDesc: string;
|
||||
};
|
||||
}) =>
|
||||
request({
|
||||
url: "/task/task/completeTask",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 转办任务
|
||||
*/
|
||||
delegateTask: (data: {
|
||||
taskId: string;
|
||||
assignee: string;
|
||||
comment?: string;
|
||||
}) =>
|
||||
request({
|
||||
url: "/task/task/delegate",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取用户选择列表
|
||||
*/
|
||||
getUserSelectionList: () =>
|
||||
request({
|
||||
url: "/task/task/getUserSelectionList",
|
||||
method: "GET",
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取部门组织架构和用户列表
|
||||
*/
|
||||
getDeptOrgUsers: (parentDeptId: number = 0) =>
|
||||
request({
|
||||
url: "/admin/dept/org",
|
||||
method: "GET",
|
||||
params: {
|
||||
type: "user",
|
||||
parentDeptId,
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* 设置转办人员
|
||||
*/
|
||||
setAssignee: (data: {
|
||||
paramMap: any;
|
||||
taskId: string;
|
||||
targetUserIdList: string[];
|
||||
taskLocalParamMap: {
|
||||
approveDesc: string;
|
||||
};
|
||||
}) =>
|
||||
request({
|
||||
url: "/task/task/setAssignee",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
}
|
||||
@@ -60,12 +60,18 @@ export default {
|
||||
},
|
||||
})
|
||||
},
|
||||
// 判断是否是商户角色
|
||||
// 判断是否是合作商家商户角色
|
||||
isMerchant: (): Promise<any> =>
|
||||
request({
|
||||
url: "/admin/role/isMchUser",
|
||||
method: "GET",
|
||||
}),
|
||||
// 判断是否是共富商城商户角色
|
||||
isMallMchUser: (): Promise<any> =>
|
||||
request({
|
||||
url: "/admin/role/isMallMchUser",
|
||||
method: "GET",
|
||||
}),
|
||||
// 判断用户移动端角色
|
||||
userRole: () =>
|
||||
request({
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 提交访客申请
|
||||
*/
|
||||
submitVisitor: (data: {
|
||||
visitorName: string;
|
||||
visitorPhone: string;
|
||||
purpose: string;
|
||||
visitDate: string;
|
||||
visitTime: string;
|
||||
hostName: string;
|
||||
remark?: string;
|
||||
}) =>
|
||||
request({
|
||||
url: '/integrated/visitor/submit',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取访客记录列表
|
||||
*/
|
||||
getVisitorList: (params: { current: number; size: number }) =>
|
||||
request({
|
||||
url: '/integrated/visitor/my-list',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取访客详情
|
||||
*/
|
||||
getVisitorDetail: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/visitor/detail/${id}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
mainIsScroll: false,
|
||||
bg: "",
|
||||
})
|
||||
const emit = defineEmits(["scroll"])
|
||||
|
||||
const state = reactive({
|
||||
navBgTitleColor_: props.navBgTitleColor,
|
||||
@@ -46,10 +47,11 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
function scroll(e) {
|
||||
if (!router.hasTabbar()) return
|
||||
if (!props.scrollHideTabbar) return
|
||||
const offset = 50
|
||||
const top = e.target.scrollTop
|
||||
// if (!router.hasTabbar()) return
|
||||
// if (!props.scrollHideTabbar) return
|
||||
// const offset = 50
|
||||
// const top = e.target.scrollTop
|
||||
emit("scroll", e)
|
||||
}
|
||||
|
||||
function defaultPage() {
|
||||
@@ -67,7 +69,7 @@ function initBackStyle() {
|
||||
break
|
||||
case "just-flot-back":
|
||||
state.visble.topSafe = true
|
||||
state.visble.topNav = false
|
||||
state.visble.topNav = true
|
||||
break
|
||||
default:
|
||||
break
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<view class="tree-container">
|
||||
<view v-for="item in nodeData" :key="item.id">
|
||||
<view
|
||||
class="tree-item"
|
||||
:style="item.id == selectedNode.id ? { borderColor: '#1879ff' } : { color: '#000' }"
|
||||
@click.stop="toCurrentNode(item)"
|
||||
>
|
||||
<image class="tree-item-icon" :src="filePath(item.picUrl)" />
|
||||
<text>{{ item.name }}</text>
|
||||
</view>
|
||||
|
||||
<node-tree
|
||||
v-model="selectedNode"
|
||||
v-if="item.children && item.children.length"
|
||||
:node-data="item.children"
|
||||
:style="{ marginLeft: `${getLevel(item.level) * 60}rpx` }"
|
||||
@currentNode="(v) => emit('currentNode', v)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { filePath } from "../utils/file"
|
||||
import NodeTree from "./node-tree.vue"
|
||||
|
||||
withDefaults(defineProps<{ nodeData: any[] }>(), { nodeData: () => [] })
|
||||
const emit = defineEmits(["currentNode"])
|
||||
const selectedNode = defineModel<any>("modelValue")
|
||||
|
||||
const toCurrentNode = (item) => {
|
||||
emit("currentNode", item)
|
||||
selectedNode.value = item
|
||||
}
|
||||
|
||||
const getLevel = (level) => {
|
||||
return level
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tree-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.tree-item {
|
||||
border: 4rpx solid #e4e7ed77;
|
||||
margin-bottom: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
.tree-item-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
</style>
|
||||
File renamed without changes.
@@ -0,0 +1,83 @@
|
||||
# 富文本编辑器组件
|
||||
|
||||
这是一个简单的富文本编辑器组件,专为移动端设计,支持Markdown语法。
|
||||
|
||||
## 组件文件
|
||||
|
||||
- `rich-text-editor.vue` - 核心编辑器组件
|
||||
- `rich-text-input.vue` - 表单输入组件
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 在表单中使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<tm-form-item name="description" label="详情描述" direction="vertical" required="">
|
||||
<rich-text-input
|
||||
v-model="form.description"
|
||||
placeholder="点击编辑详情描述..."
|
||||
label="详情描述"
|
||||
/>
|
||||
</tm-form-item>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RichTextInput from '@/sheep/components/rich-text-input/rich-text-input.vue'
|
||||
|
||||
const form = reactive({
|
||||
description: ''
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 直接使用编辑器
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<rich-text-editor
|
||||
v-model="content"
|
||||
placeholder="请输入内容..."
|
||||
@change="onContentChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RichTextEditor from '@/sheep/components/rich-text-editor/rich-text-editor.vue'
|
||||
|
||||
const content = ref('')
|
||||
|
||||
const onContentChange = (newContent) => {
|
||||
console.log('内容变化:', newContent)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 支持Markdown语法(标题、粗体、斜体、列表、表格、链接等)
|
||||
- ✅ 实时预览功能
|
||||
- ✅ 模板插入功能
|
||||
- ✅ 移动端优化界面
|
||||
- ✅ 弹窗编辑模式
|
||||
- ✅ 表单集成
|
||||
|
||||
## 支持的Markdown语法
|
||||
|
||||
- `# 标题` - 一级标题
|
||||
- `## 标题` - 二级标题
|
||||
- `### 标题` - 三级标题
|
||||
- `**粗体**` - 粗体文字
|
||||
- `*斜体*` - 斜体文字
|
||||
- `- 列表项` - 无序列表
|
||||
- `> 引用` - 引用块
|
||||
- `[链接文字](URL)` - 链接
|
||||
- `` - 图片
|
||||
- `| 表格 |` - 表格
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 组件使用tm-ui的Modal组件,确保项目中已安装tm-ui
|
||||
2. 图片上传功能需要配合项目的上传接口
|
||||
3. 组件样式可以根据项目需求进行调整
|
||||
4. 没有修改任何配置文件,只创建了组件文件
|
||||
@@ -0,0 +1,724 @@
|
||||
<template>
|
||||
<view class="rich-text-editor">
|
||||
<!-- 工具栏 -->
|
||||
<view class="toolbar">
|
||||
<view class="toolbar-row">
|
||||
<view class="tool-item" @click="insertBold">
|
||||
<text class="tool-text">粗体</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="insertItalic">
|
||||
<text class="tool-text">斜体</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="insertTitle">
|
||||
<text class="tool-text">标题</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="insertList">
|
||||
<text class="tool-text">列表</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="toolbar-row">
|
||||
<view class="tool-item" @click="insertLink">
|
||||
<text class="tool-text">链接</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="insertImage">
|
||||
<text class="tool-text">图片</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="insertTable">
|
||||
<text class="tool-text">表格</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="clearFormat">
|
||||
<text class="tool-text">清空</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 编辑区域 -->
|
||||
<view class="editor-container">
|
||||
<textarea
|
||||
v-model="content"
|
||||
class="editor-textarea"
|
||||
:placeholder="placeholder"
|
||||
@input="onContentChange"
|
||||
></textarea>
|
||||
</view>
|
||||
|
||||
<!-- 预览区域 -->
|
||||
<view v-if="showPreview" class="preview-container">
|
||||
<view class="preview-title">预览效果</view>
|
||||
<view class="preview-content">
|
||||
<rich-text :nodes="previewNodes"></rich-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-actions">
|
||||
<tm-button size="s" skin="thin" @click="togglePreview">
|
||||
{{ showPreview ? '编辑' : '预览' }}
|
||||
</tm-button>
|
||||
<tm-button size="s" skin="thin" @click="insertTemplate">模板</tm-button>
|
||||
</view>
|
||||
|
||||
<!-- 链接输入弹窗 -->
|
||||
<tm-modal v-model:show="showLinkModal" title="插入链接" :showClose="true" size="small" :zIndex="1001">
|
||||
<view class="link-modal-content">
|
||||
<tm-input v-model="linkUrl" placeholder="请输入链接地址" class="link-input"></tm-input>
|
||||
</view>
|
||||
<template #footer>
|
||||
<view class="modal-footer">
|
||||
<tm-button size="s" skin="thin" @click="showLinkModal = false">取消</tm-button>
|
||||
<tm-button size="s" color="primary" @click="confirmLink">确定</tm-button>
|
||||
</view>
|
||||
</template>
|
||||
</tm-modal>
|
||||
|
||||
<!-- 模板选择弹窗 -->
|
||||
<tm-modal v-model:show="showTemplateModal" title="选择模板" :showClose="true" size="large" :zIndex="1001">
|
||||
<view class="template-modal-content">
|
||||
<view class="template-list">
|
||||
<view
|
||||
v-for="(template, index) in templates"
|
||||
:key="index"
|
||||
class="template-item"
|
||||
@click="selectTemplate(template)"
|
||||
>
|
||||
<text class="template-title">{{ template.title }}</text>
|
||||
<text class="template-desc">{{ template.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<template #footer>
|
||||
<view class="modal-footer">
|
||||
<tm-button size="s" skin="thin" @click="showTemplateModal = false">取消</tm-button>
|
||||
</view>
|
||||
</template>
|
||||
</tm-modal>
|
||||
|
||||
<!-- 图片描述输入弹窗 -->
|
||||
<tm-modal v-model:show="showImageModal" title="插入图片" :showClose="true" size="small" :zIndex="1001">
|
||||
<view class="image-modal-content">
|
||||
<tm-input v-model="imageDescription" placeholder="请输入图片描述" class="image-input"></tm-input>
|
||||
</view>
|
||||
<template #footer>
|
||||
<view class="modal-footer">
|
||||
<tm-button size="s" skin="thin" @click="showImageModal = false">取消</tm-button>
|
||||
<tm-button size="s" color="primary" @click="confirmImage">确定</tm-button>
|
||||
</view>
|
||||
</template>
|
||||
</tm-modal>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { hooks } from '@@/index'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
height?: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'change', value: string): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
placeholder: '请输入内容...',
|
||||
height: '300px'
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const content = ref(props.modelValue)
|
||||
const showPreview = ref(false)
|
||||
const showLinkModal = ref(false)
|
||||
const showTemplateModal = ref(false)
|
||||
const showImageModal = ref(false)
|
||||
const linkUrl = ref('')
|
||||
const imageDescription = ref('')
|
||||
const pendingImageUrl = ref('')
|
||||
|
||||
// 模板数据
|
||||
const templates = ref([
|
||||
{
|
||||
title: '商品介绍模板',
|
||||
desc: '包含产品特点、规格、使用方法等',
|
||||
content: `# 商品介绍
|
||||
|
||||
## 产品特点
|
||||
- 特点1:详细描述
|
||||
- 特点2:详细描述
|
||||
- 特点3:详细描述
|
||||
|
||||
## 产品规格
|
||||
| 规格 | 参数 | 说明 |
|
||||
|------|------|------|
|
||||
| 尺寸 | 长x宽x高 | 具体尺寸 |
|
||||
| 重量 | 重量 | 具体重量 |
|
||||
| 材质 | 材质 | 具体材质 |
|
||||
|
||||
## 使用方法
|
||||
1. **步骤一**:详细说明
|
||||
2. **步骤二**:详细说明
|
||||
3. **步骤三**:详细说明
|
||||
|
||||
## 注意事项
|
||||
> 请仔细阅读以下注意事项:
|
||||
> - 注意事项1
|
||||
> - 注意事项2
|
||||
> - 注意事项3`
|
||||
},
|
||||
{
|
||||
title: '活动说明模板',
|
||||
desc: '包含活动时间、规则、奖品设置等',
|
||||
content: `# 活动说明
|
||||
|
||||
## 活动时间
|
||||
**活动开始:** 2024年1月1日 00:00
|
||||
**活动结束:** 2024年1月31日 23:59
|
||||
|
||||
## 活动规则
|
||||
1. **参与条件**:详细说明参与条件
|
||||
2. **活动流程**:详细说明活动流程
|
||||
3. **奖励机制**:详细说明奖励机制
|
||||
|
||||
## 奖品设置
|
||||
| 奖项 | 奖品名称 | 数量 | 价值 |
|
||||
|------|----------|------|------|
|
||||
| 一等奖 | 奖品1 | 1个 | ¥1000 |
|
||||
| 二等奖 | 奖品2 | 3个 | ¥500 |
|
||||
| 三等奖 | 奖品3 | 10个 | ¥100`
|
||||
},
|
||||
{
|
||||
title: '使用说明模板',
|
||||
desc: '包含安装步骤、功能介绍、常见问题等',
|
||||
content: `# 使用说明
|
||||
|
||||
## 安装步骤
|
||||
1. **下载安装包**
|
||||
- 访问官网下载最新版本
|
||||
- 确保系统兼容性
|
||||
|
||||
2. **运行安装程序**
|
||||
- 双击安装包
|
||||
- 按照提示完成安装
|
||||
|
||||
3. **首次配置**
|
||||
- 启动程序
|
||||
- 完成基本设置
|
||||
|
||||
## 功能介绍
|
||||
- **功能一**:详细功能说明
|
||||
- **功能二**:详细功能说明
|
||||
- **功能三**:详细功能说明
|
||||
|
||||
## 常见问题
|
||||
**Q: 问题一?**
|
||||
A: 详细解答
|
||||
|
||||
**Q: 问题二?**
|
||||
A: 详细解答
|
||||
|
||||
**Q: 问题三?**
|
||||
A: 详细解答`
|
||||
}
|
||||
])
|
||||
|
||||
// 预览节点
|
||||
const previewNodes = computed(() => {
|
||||
if (!content.value) return []
|
||||
return parseToRichText(content.value)
|
||||
})
|
||||
|
||||
// 内容变化
|
||||
const onContentChange = () => {
|
||||
emit('update:modelValue', content.value)
|
||||
emit('change', content.value)
|
||||
}
|
||||
|
||||
// 插入粗体
|
||||
const insertBold = () => {
|
||||
insertText('**粗体文字**')
|
||||
}
|
||||
|
||||
// 插入斜体
|
||||
const insertItalic = () => {
|
||||
insertText('*斜体文字*')
|
||||
}
|
||||
|
||||
// 插入标题
|
||||
const insertTitle = () => {
|
||||
insertText('# 标题文字')
|
||||
}
|
||||
|
||||
// 插入列表
|
||||
const insertList = () => {
|
||||
insertText('- 列表项1\n- 列表项2\n- 列表项3')
|
||||
}
|
||||
|
||||
// 插入链接
|
||||
const insertLink = () => {
|
||||
linkUrl.value = ''
|
||||
showLinkModal.value = true
|
||||
}
|
||||
|
||||
// 确认插入链接
|
||||
const confirmLink = () => {
|
||||
if (linkUrl.value.trim()) {
|
||||
insertText(`[链接文字](${linkUrl.value})`)
|
||||
showLinkModal.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 插入图片
|
||||
const insertImage = () => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
zIndex: 1001,
|
||||
success: (res) => {
|
||||
// 先上传图片
|
||||
uploadImage(res.tempFilePaths[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
const uploadImage = (filePath: string) => {
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
|
||||
// 使用项目中的hooks获取请求头
|
||||
const headers = hooks.useDefaultHeader()
|
||||
|
||||
uni.uploadFile({
|
||||
url: '/api/admin/sys-file/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
header: headers,
|
||||
success: (res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.code === 0 && data.data && data.data.url) {
|
||||
// 修复URL格式问题,确保fileName参数格式正确
|
||||
let imageUrl = data.data.url
|
||||
// 如果URL中包含fileName参数但没有等号,则修复它
|
||||
if (imageUrl.includes('fileName ') && !imageUrl.includes('fileName=')) {
|
||||
imageUrl = imageUrl.replace('fileName ', 'fileName=')
|
||||
}
|
||||
|
||||
// 存储时保持相对路径,不拼接完整URL
|
||||
// 只在显示时才拼接完整URL
|
||||
|
||||
// 上传成功,显示描述输入弹窗
|
||||
// 确保存储的是相对路径
|
||||
pendingImageUrl.value = imageUrl
|
||||
imageDescription.value = ''
|
||||
showImageModal.value = true
|
||||
uni.showToast({ title: '上传成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: data.msg || '上传失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
},
|
||||
complete: () => {
|
||||
uni.hideLoading()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 确认插入图片
|
||||
const confirmImage = () => {
|
||||
if (pendingImageUrl.value) {
|
||||
const desc = imageDescription.value.trim() || '图片'
|
||||
// 确保存储的是相对路径,移除可能存在的完整URL前缀
|
||||
let imageUrl = pendingImageUrl.value
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
// 如果是完整URL,提取相对路径部分
|
||||
const url = new URL(imageUrl)
|
||||
imageUrl = url.pathname + url.search
|
||||
}
|
||||
insertText(``)
|
||||
showImageModal.value = false
|
||||
pendingImageUrl.value = ''
|
||||
imageDescription.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 插入表格
|
||||
const insertTable = () => {
|
||||
const tableText = `| 列1 | 列2 | 列3 |
|
||||
|-----|-----|-----|
|
||||
| 内容1 | 内容2 | 内容3 |`
|
||||
insertText(tableText)
|
||||
}
|
||||
|
||||
// 清空格式
|
||||
const clearFormat = () => {
|
||||
content.value = content.value
|
||||
.replace(/\*\*(.*?)\*\*/g, '$1')
|
||||
.replace(/\*(.*?)\*/g, '$1')
|
||||
}
|
||||
|
||||
// 切换预览
|
||||
const togglePreview = () => {
|
||||
showPreview.value = !showPreview.value
|
||||
}
|
||||
|
||||
// 插入模板
|
||||
const insertTemplate = () => {
|
||||
showTemplateModal.value = true
|
||||
}
|
||||
|
||||
// 选择模板
|
||||
const selectTemplate = (template: any) => {
|
||||
content.value = template.content
|
||||
onContentChange()
|
||||
showTemplateModal.value = false
|
||||
}
|
||||
|
||||
// 插入文本
|
||||
const insertText = (text: string) => {
|
||||
content.value += '\n' + text + '\n'
|
||||
onContentChange()
|
||||
}
|
||||
|
||||
// 解析为富文本节点
|
||||
const parseToRichText = (text: string) => {
|
||||
if (!text) return []
|
||||
|
||||
const nodes = []
|
||||
const lines = text.split('\n')
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
|
||||
if (line.startsWith('# ')) {
|
||||
nodes.push({
|
||||
name: 'h1',
|
||||
attrs: { style: 'font-size: 18px; font-weight: bold; margin: 8px 0; color: #333;' },
|
||||
children: [{ type: 'text', text: line.substring(2) }]
|
||||
})
|
||||
} else if (line.startsWith('## ')) {
|
||||
nodes.push({
|
||||
name: 'h2',
|
||||
attrs: { style: 'font-size: 16px; font-weight: bold; margin: 6px 0; color: #333;' },
|
||||
children: [{ type: 'text', text: line.substring(3) }]
|
||||
})
|
||||
} else if (line.startsWith('### ')) {
|
||||
nodes.push({
|
||||
name: 'h3',
|
||||
attrs: { style: 'font-size: 14px; font-weight: bold; margin: 4px 0; color: #333;' },
|
||||
children: [{ type: 'text', text: line.substring(4) }]
|
||||
})
|
||||
} else if (line.startsWith('- ')) {
|
||||
nodes.push({
|
||||
name: 'li',
|
||||
attrs: { style: 'margin: 2px 0; padding-left: 16px; color: #666;' },
|
||||
children: [{ type: 'text', text: '• ' + line.substring(2) }]
|
||||
})
|
||||
} else if (line.startsWith('> ')) {
|
||||
nodes.push({
|
||||
name: 'blockquote',
|
||||
attrs: { style: 'border-left: 3px solid #ddd; padding-left: 8px; margin: 4px 0; color: #666; font-style: italic;' },
|
||||
children: [{ type: 'text', text: line.substring(2) }]
|
||||
})
|
||||
} else if (line.startsWith('---')) {
|
||||
nodes.push({
|
||||
name: 'hr',
|
||||
attrs: { style: 'border: none; border-top: 1px solid #eee; margin: 8px 0;' }
|
||||
})
|
||||
} else if (line.trim()) {
|
||||
const formattedText = formatText(line)
|
||||
nodes.push({
|
||||
name: 'p',
|
||||
attrs: { style: 'margin: 2px 0; line-height: 1.4; color: #666;' },
|
||||
children: formattedText
|
||||
})
|
||||
} else {
|
||||
nodes.push({
|
||||
name: 'br'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// 格式化文本
|
||||
const formatText = (text: string) => {
|
||||
const children = []
|
||||
let remaining = text
|
||||
|
||||
while (remaining.length > 0) {
|
||||
const boldMatch = remaining.match(/\*\*(.*?)\*\*/)
|
||||
const italicMatch = remaining.match(/\*(.*?)\*/)
|
||||
const linkMatch = remaining.match(/\[([^\]]+)\]\(([^)]+)\)/)
|
||||
const imageMatch = remaining.match(/!\[([^\]]*)\]\(([^)]+)\)/)
|
||||
|
||||
let match = null
|
||||
let type = ''
|
||||
|
||||
if (boldMatch && (!match || boldMatch.index < match.index)) {
|
||||
match = boldMatch
|
||||
type = 'bold'
|
||||
}
|
||||
if (italicMatch && (!match || italicMatch.index < match.index)) {
|
||||
match = italicMatch
|
||||
type = 'italic'
|
||||
}
|
||||
if (linkMatch && (!match || linkMatch.index < match.index)) {
|
||||
match = linkMatch
|
||||
type = 'link'
|
||||
}
|
||||
if (imageMatch && (!match || imageMatch.index < match.index)) {
|
||||
match = imageMatch
|
||||
type = 'image'
|
||||
}
|
||||
|
||||
if (match) {
|
||||
if (match.index > 0) {
|
||||
children.push({
|
||||
type: 'text',
|
||||
text: remaining.substring(0, match.index)
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'bold') {
|
||||
children.push({
|
||||
name: 'strong',
|
||||
children: [{ type: 'text', text: match[1] }]
|
||||
})
|
||||
} else if (type === 'italic') {
|
||||
children.push({
|
||||
name: 'em',
|
||||
children: [{ type: 'text', text: match[1] }]
|
||||
})
|
||||
} else if (type === 'link') {
|
||||
children.push({
|
||||
name: 'a',
|
||||
attrs: { href: match[2], style: 'color: #007bff; text-decoration: underline;' },
|
||||
children: [{ type: 'text', text: match[1] }]
|
||||
})
|
||||
} else if (type === 'image') {
|
||||
// 处理图片URL,确保能正确显示
|
||||
let imageUrl = match[2]
|
||||
console.log('富文本编辑器 - 原始图片URL:', imageUrl)
|
||||
// 如果是相对路径,添加基础URL
|
||||
if (imageUrl.startsWith('/api/') || imageUrl.startsWith('/admin/')) {
|
||||
// 使用环境变量获取图片基础路径
|
||||
const isDev = import.meta.env.DEV
|
||||
let baseUrl = ''
|
||||
|
||||
if (isDev) {
|
||||
// 开发环境
|
||||
baseUrl = import.meta.env.VITE_IMAGE_DEV_PATH || 'http://127.0.0.1:9999'
|
||||
} else {
|
||||
// 生产环境
|
||||
baseUrl = import.meta.env.VITE_APP_PROD_IMAGE || window.location.origin
|
||||
}
|
||||
|
||||
console.log('富文本编辑器 - 环境:', isDev ? '开发' : '生产')
|
||||
console.log('富文本编辑器 - 基础URL:', baseUrl)
|
||||
console.log('富文本编辑器 - 原始图片URL:', imageUrl)
|
||||
|
||||
// 拼接URL
|
||||
imageUrl = baseUrl + imageUrl
|
||||
console.log('富文本编辑器 - 拼接后URL:', imageUrl)
|
||||
}
|
||||
|
||||
children.push({
|
||||
name: 'img',
|
||||
attrs: {
|
||||
src: imageUrl,
|
||||
alt: match[1] || '图片',
|
||||
style: 'max-width: 100%; height: auto; display: block; margin: 8px 0; border-radius: 4px;'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
remaining = remaining.substring(match.index + match[0].length)
|
||||
} else {
|
||||
children.push({
|
||||
type: 'text',
|
||||
text: remaining
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return children.length > 0 ? children : [{ type: 'text', text: text }]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rich-text-editor {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e9ecef;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
padding: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.toolbar-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
border: 1px solid #e9ecef;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex: 1;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.tool-item:active {
|
||||
background: #e9ecef;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.tool-text {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editor-textarea {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
resize: none;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
border-top: 1px solid #e9ecef;
|
||||
padding: 12px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e9ecef;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.link-modal-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.link-input {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.image-modal-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.image-input {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.template-modal-content {
|
||||
padding: 16px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.template-item {
|
||||
padding: 16px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.template-item:active {
|
||||
background: #f8f9fa;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.template-title {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.template-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,409 @@
|
||||
<template>
|
||||
<view class="rich-text-input">
|
||||
<tm-form-item :name="name" :label="label" :required="required" :direction="direction">
|
||||
<view class="input-container" @click="openEditor">
|
||||
<view v-if="!modelValue || modelValue.trim() === ''" class="placeholder">
|
||||
{{ placeholder }}
|
||||
</view>
|
||||
<view v-else class="content-preview">
|
||||
<rich-text :nodes="previewNodes" class="preview-text"></rich-text>
|
||||
</view>
|
||||
<view class="input-actions">
|
||||
<tm-icon name="edit" size="16" color="#999"></tm-icon>
|
||||
</view>
|
||||
</view>
|
||||
</tm-form-item>
|
||||
|
||||
<!-- 弹窗编辑器 -->
|
||||
<tm-modal v-model:show="showEditor" title="富文本编辑" :showClose="true" size="large" :height="'80vh'" :zIndex="1000">
|
||||
<view class="editor-modal-content">
|
||||
<rich-text-editor
|
||||
v-model="editorContent"
|
||||
:placeholder="placeholder"
|
||||
@change="onEditorChange"
|
||||
/>
|
||||
</view>
|
||||
<template #footer>
|
||||
<view class="modal-footer">
|
||||
<tm-button size="s" skin="thin" @click="showEditor = false">取消</tm-button>
|
||||
<tm-button size="s" color="primary" @click="saveContent">保存</tm-button>
|
||||
</view>
|
||||
</template>
|
||||
</tm-modal>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import RichTextEditor from '../rich-text-editor/rich-text-editor.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string
|
||||
label?: string
|
||||
name?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
direction?: 'horizontal' | 'vertical'
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'change', value: string): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
label: '内容',
|
||||
placeholder: '点击编辑内容...',
|
||||
required: false,
|
||||
direction: 'horizontal'
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const showEditor = ref(false)
|
||||
const editorContent = ref('')
|
||||
|
||||
// 预览节点
|
||||
const previewNodes = computed(() => {
|
||||
if (!props.modelValue || props.modelValue.trim() === '') return []
|
||||
return parseToRichText(props.modelValue)
|
||||
})
|
||||
|
||||
// 监听内容变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
editorContent.value = newVal || ''
|
||||
})
|
||||
|
||||
// 打开编辑器
|
||||
const openEditor = () => {
|
||||
editorContent.value = props.modelValue || ''
|
||||
showEditor.value = true
|
||||
}
|
||||
|
||||
// 编辑器内容变化
|
||||
const onEditorChange = (content: string) => {
|
||||
editorContent.value = content
|
||||
}
|
||||
|
||||
// 保存内容
|
||||
const saveContent = () => {
|
||||
emit('update:modelValue', editorContent.value)
|
||||
emit('change', editorContent.value)
|
||||
showEditor.value = false
|
||||
}
|
||||
|
||||
// HTML转Markdown函数
|
||||
const convertHtmlToMarkdown = (html: string): string => {
|
||||
if (!html) return ''
|
||||
|
||||
// 移除HTML标签,保留文本内容
|
||||
let markdown = html
|
||||
// 处理标题
|
||||
.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n')
|
||||
.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n')
|
||||
.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n')
|
||||
.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '#### $1\n')
|
||||
.replace(/<h5[^>]*>(.*?)<\/h5>/gi, '##### $1\n')
|
||||
.replace(/<h6[^>]*>(.*?)<\/h6>/gi, '###### $1\n')
|
||||
// 处理段落
|
||||
.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n')
|
||||
// 处理换行
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
// 处理粗体
|
||||
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
|
||||
.replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**')
|
||||
// 处理斜体
|
||||
.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
|
||||
.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*')
|
||||
// 处理下划线
|
||||
.replace(/<u[^>]*>(.*?)<\/u>/gi, '__$1__')
|
||||
// 处理链接
|
||||
.replace(/<a[^>]*href=["']([^"']*)["'][^>]*>(.*?)<\/a>/gi, '[$2]($1)')
|
||||
// 处理图片 - 更全面的图片处理
|
||||
.replace(/<img[^>]*src=["']([^"']*)["'][^>]*alt=["']([^"']*)["'][^>]*>/gi, (match, src, alt) => {
|
||||
// 直接使用原始URL,不添加额外前缀
|
||||
console.log('rich-text-input HTML图片转换 - 原始:', match)
|
||||
console.log('rich-text-input HTML图片转换 - src:', src)
|
||||
console.log('rich-text-input HTML图片转换 - alt:', alt)
|
||||
return ``
|
||||
})
|
||||
.replace(/<img[^>]*src=["']([^"']*)["'][^>]*>/gi, (match, src) => {
|
||||
// 直接使用原始URL,不添加额外前缀
|
||||
console.log('rich-text-input HTML图片转换 - 原始:', match)
|
||||
console.log('rich-text-input HTML图片转换 - src:', src)
|
||||
return ``
|
||||
})
|
||||
// 处理列表
|
||||
.replace(/<ul[^>]*>(.*?)<\/ul>/gis, (match, content) => {
|
||||
return content.replace(/<li[^>]*>(.*?)<\/li>/gi, '- $1\n')
|
||||
})
|
||||
.replace(/<ol[^>]*>(.*?)<\/ol>/gis, (match, content) => {
|
||||
let index = 1
|
||||
return content.replace(/<li[^>]*>(.*?)<\/li>/gi, () => `${index++}. $1\n`)
|
||||
})
|
||||
// 处理引用
|
||||
.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gis, (match, content) => {
|
||||
return content.replace(/\n/g, '\n> ') + '\n'
|
||||
})
|
||||
// 处理代码
|
||||
.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`')
|
||||
.replace(/<pre[^>]*>(.*?)<\/pre>/gis, '```\n$1\n```')
|
||||
// 处理分割线
|
||||
.replace(/<hr[^>]*>/gi, '---\n')
|
||||
// 处理表格(简单处理)
|
||||
.replace(/<table[^>]*>(.*?)<\/table>/gis, (match, content) => {
|
||||
return content.replace(/<tr[^>]*>(.*?)<\/tr>/gi, (trMatch, trContent) => {
|
||||
const cells = trContent.match(/<t[hd][^>]*>(.*?)<\/t[hd]>/gi)
|
||||
if (cells) {
|
||||
return '| ' + cells.map(cell => cell.replace(/<[^>]*>/g, '')).join(' | ') + ' |\n'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
})
|
||||
// 移除剩余的HTML标签
|
||||
.replace(/<[^>]*>/g, '')
|
||||
// 清理多余的换行
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
|
||||
return markdown
|
||||
}
|
||||
|
||||
// 解析为富文本节点
|
||||
const parseToRichText = (text: string) => {
|
||||
if (!text) return []
|
||||
|
||||
// 如果包含HTML标签,先转换为Markdown
|
||||
const markdownText = text.includes('<') ? convertHtmlToMarkdown(text) : text
|
||||
|
||||
const nodes = []
|
||||
const lines = markdownText.split('\n')
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
|
||||
if (line.startsWith('# ')) {
|
||||
nodes.push({
|
||||
name: 'h1',
|
||||
attrs: { style: 'font-size: 16px; font-weight: bold; margin: 6px 0; color: #333;' },
|
||||
children: [{ type: 'text', text: line.substring(2) }]
|
||||
})
|
||||
} else if (line.startsWith('## ')) {
|
||||
nodes.push({
|
||||
name: 'h2',
|
||||
attrs: { style: 'font-size: 14px; font-weight: bold; margin: 4px 0; color: #333;' },
|
||||
children: [{ type: 'text', text: line.substring(3) }]
|
||||
})
|
||||
} else if (line.startsWith('### ')) {
|
||||
nodes.push({
|
||||
name: 'h3',
|
||||
attrs: { style: 'font-size: 13px; font-weight: bold; margin: 3px 0; color: #333;' },
|
||||
children: [{ type: 'text', text: line.substring(4) }]
|
||||
})
|
||||
} else if (line.startsWith('- ')) {
|
||||
nodes.push({
|
||||
name: 'li',
|
||||
attrs: { style: 'margin: 1px 0; padding-left: 12px; color: #666;' },
|
||||
children: [{ type: 'text', text: '• ' + line.substring(2) }]
|
||||
})
|
||||
} else if (line.startsWith('> ')) {
|
||||
nodes.push({
|
||||
name: 'blockquote',
|
||||
attrs: { style: 'border-left: 2px solid #ddd; padding-left: 6px; margin: 2px 0; color: #666; font-style: italic;' },
|
||||
children: [{ type: 'text', text: line.substring(2) }]
|
||||
})
|
||||
} else if (line.startsWith('---')) {
|
||||
nodes.push({
|
||||
name: 'hr',
|
||||
attrs: { style: 'border: none; border-top: 1px solid #eee; margin: 6px 0;' }
|
||||
})
|
||||
} else if (line.trim()) {
|
||||
const formattedText = formatText(line)
|
||||
nodes.push({
|
||||
name: 'p',
|
||||
attrs: { style: 'margin: 1px 0; line-height: 1.3; color: #666;' },
|
||||
children: formattedText
|
||||
})
|
||||
} else {
|
||||
nodes.push({
|
||||
name: 'br'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// 格式化文本
|
||||
const formatText = (text: string) => {
|
||||
const children = []
|
||||
let remaining = text
|
||||
|
||||
while (remaining.length > 0) {
|
||||
const boldMatch = remaining.match(/\*\*(.*?)\*\*/)
|
||||
const italicMatch = remaining.match(/\*(.*?)\*/)
|
||||
const linkMatch = remaining.match(/\[([^\]]+)\]\(([^)]+)\)/)
|
||||
const imageMatch = remaining.match(/!\[([^\]]*)\]\(([^)]+)\)/)
|
||||
|
||||
let match = null
|
||||
let type = ''
|
||||
|
||||
if (boldMatch && (!match || boldMatch.index < match.index)) {
|
||||
match = boldMatch
|
||||
type = 'bold'
|
||||
}
|
||||
if (italicMatch && (!match || italicMatch.index < match.index)) {
|
||||
match = italicMatch
|
||||
type = 'italic'
|
||||
}
|
||||
if (linkMatch && (!match || linkMatch.index < match.index)) {
|
||||
match = linkMatch
|
||||
type = 'link'
|
||||
}
|
||||
if (imageMatch && (!match || imageMatch.index < match.index)) {
|
||||
match = imageMatch
|
||||
type = 'image'
|
||||
}
|
||||
|
||||
if (match) {
|
||||
if (match.index > 0) {
|
||||
children.push({
|
||||
type: 'text',
|
||||
text: remaining.substring(0, match.index)
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'bold') {
|
||||
children.push({
|
||||
name: 'strong',
|
||||
children: [{ type: 'text', text: match[1] }]
|
||||
})
|
||||
} else if (type === 'italic') {
|
||||
children.push({
|
||||
name: 'em',
|
||||
children: [{ type: 'text', text: match[1] }]
|
||||
})
|
||||
} else if (type === 'link') {
|
||||
children.push({
|
||||
name: 'a',
|
||||
attrs: { href: match[2], style: 'color: #007bff; text-decoration: underline;' },
|
||||
children: [{ type: 'text', text: match[1] }]
|
||||
})
|
||||
} else if (type === 'image') {
|
||||
// 处理图片URL,确保能正确显示
|
||||
let imageUrl = match[2]
|
||||
console.log('原始图片URL:', imageUrl)
|
||||
// 如果是相对路径,添加基础URL
|
||||
if (imageUrl.startsWith('/api/') || imageUrl.startsWith('/admin/')) {
|
||||
// 使用环境变量获取图片基础路径
|
||||
const isDev = import.meta.env.DEV
|
||||
let baseUrl = ''
|
||||
|
||||
if (isDev) {
|
||||
// 开发环境
|
||||
baseUrl = import.meta.env.VITE_IMAGE_DEV_PATH || 'http://127.0.0.1:9999'
|
||||
} else {
|
||||
// 生产环境
|
||||
baseUrl = import.meta.env.VITE_APP_PROD_IMAGE || window.location.origin
|
||||
}
|
||||
|
||||
console.log('环境:', isDev ? '开发' : '生产')
|
||||
console.log('基础URL:', baseUrl)
|
||||
console.log('原始图片URL:', imageUrl)
|
||||
|
||||
// 拼接URL
|
||||
imageUrl = baseUrl + imageUrl
|
||||
console.log('拼接后URL:', imageUrl)
|
||||
}
|
||||
|
||||
children.push({
|
||||
name: 'img',
|
||||
attrs: {
|
||||
src: imageUrl,
|
||||
alt: match[1] || '图片',
|
||||
style: 'max-width: 100%; height: auto; display: block; margin: 8px 0; border-radius: 4px;'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
remaining = remaining.substring(match.index + match[0].length)
|
||||
} else {
|
||||
children.push({
|
||||
type: 'text',
|
||||
text: remaining
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return children.length > 0 ? children : [{ type: 'text', text: text }]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rich-text-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
min-height: 44px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.input-container:active {
|
||||
border-color: #007bff;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.content-preview {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
max-height: 80px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-modal-content {
|
||||
height: 60vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<view class="mb-20rpx">
|
||||
<node-tree v-model="selectedNode" :node-data="categoryTree" />
|
||||
<view v-if="categoryTree.length === 0" class="text-center text-#888 py-40rpx">暂无数据</view>
|
||||
</view>
|
||||
<view class="px-20rpx py-2 flex justify-between sticky bottom-0 bg-white">
|
||||
<tm-button skin="thin" block @click="emit('cancel')">取消</tm-button>
|
||||
|
||||
<tm-button block :disabled="!selectedNode" @click="onConfirm">确定</tm-button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import NodeTree from "@/sheep/components/node-tree.vue"
|
||||
|
||||
defineProps({
|
||||
categoryTree: {
|
||||
type: Array<any>,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(["confirm", "cancel"])
|
||||
|
||||
const selectedNode = ref<any>({})
|
||||
|
||||
function onConfirm() {
|
||||
if (!selectedNode.value) {
|
||||
return uni.showToast({ title: "请选择分类", icon: "none" })
|
||||
}
|
||||
emit("confirm", { ...selectedNode, categoryId: selectedNode.value.id, categoryName: selectedNode.value.name })
|
||||
}
|
||||
</script>
|
||||
File renamed without changes.
+1
-1
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed, onUnmounted } from "vue"
|
||||
import { ref, reactive, watch, onUnmounted } from "vue"
|
||||
import { userApi } from "@@/api"
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h, render } from "vue"
|
||||
import VerifyDeviceImpModel from "./verifyDeviceImpModel.vue"
|
||||
import VerifyDeviceImpModel from "./verify-device-imp-model.vue"
|
||||
|
||||
let VerifyDeviceImpModelInstance: boolean | null = null
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
export * from "./use-copy";
|
||||
export * from "./use-copy"
|
||||
|
||||
export * from "./use-enum";
|
||||
export * from "./use-enum"
|
||||
|
||||
export * from "./use-img";
|
||||
export * from "./use-img"
|
||||
|
||||
export * from "./use-page";
|
||||
export * from "./use-page"
|
||||
|
||||
export * from "./use-phone";
|
||||
export * from "./use-phone"
|
||||
|
||||
export * from "./use-unit";
|
||||
export * from "./use-unit"
|
||||
|
||||
export * from "./use-token";
|
||||
export * from "./use-token"
|
||||
|
||||
export * from "./use-scan-code"
|
||||
Whitespace-only changes.
@@ -0,0 +1,105 @@
|
||||
import { router } from ".."
|
||||
import { APPENV } from "../constant"
|
||||
import { getAppInfo, isDevEnv } from "../utils/app"
|
||||
|
||||
export function useScanCode() {
|
||||
async function scanCodeAction(): Promise<string | undefined> {
|
||||
const appInfo = await getAppInfo()
|
||||
|
||||
if (isDevEnv()) {
|
||||
// 开发环境模拟扫码
|
||||
// const testQrCode = "eyJpZCI6IjE5MDU4ODg4NjQ0NTQ1MzMxMjEiLCJuYW1lIjoi6ICB5Y6/5bqc6aOf5aCCIn0=" // 测试线上-老县府食堂-收款二维码
|
||||
// const testQrCode = "eyJoYXJkRG9vcklkIjoiMTkxNDIxNTYwODQzNzQxMTg0MSIsImNhYmluZXRObyI6IjEifQ==" // 测试线上-智能柜-收款二维码
|
||||
// const testQrCode = "eyJpZCI6IjE5MDU4MTY3NTEwNDM0NTI5MzAiLCJuYW1lIjoi5paw5Y6/5bqc6LaF5biCIn0=" // 邓本地-测试环境-新县府超市收款二维码
|
||||
// const testQrCode = "eyJoYXJkRG9vcklkIjoiMTkxMzE1NzA4MDQxODI5NTgwOSIsImNhYmluZXRObyI6IjEifQ==" // 邓本地-测试单门智能柜柜门二维码
|
||||
const testQrCode = "http://192.168.101.45:8889/h5/#/pages/pay/scanPay?id=1905816751043452930&name=测试新县府超市" // 邓本地-测试环境-新县府超市收款二维码
|
||||
// const testQrCode = "http://192.168.101.45:8889/h5/#/pages/pay/scanPay?hardDoorId=1913157080418295809&cabinetNo=1" // 测试智能柜柜门二维码
|
||||
uni.showModal({ content: "开发环境不是浙政钉和诗画环境,扫码仅供测试" })
|
||||
return Promise.resolve(testQrCode)
|
||||
}
|
||||
|
||||
if (appInfo == APPENV.ZZD) {
|
||||
// 浙政钉扫码
|
||||
const res: any = await (window as any).dd.scan({ type: "all" })
|
||||
return res.text || ""
|
||||
}
|
||||
|
||||
if (appInfo == APPENV.SH) {
|
||||
// 诗画扫码
|
||||
const res = await (window as any).CP2.showScanQr({ needResult: 1 })
|
||||
if (res.code == 1) return res.data.qrCode || ""
|
||||
return ""
|
||||
}
|
||||
uni.showToast({ title: "当前环境未知", icon: "none" })
|
||||
}
|
||||
|
||||
// 处理url扫码结果
|
||||
function processUrlScanResult(url: string) {
|
||||
if (!url) return
|
||||
const searchParams = new URL(url).searchParams
|
||||
const paramsObj: Record<string, string> = {}
|
||||
searchParams.forEach((value, key) => (paramsObj[key] = value))
|
||||
const { hardDoorId, cabinetNo, action, id, name } = paramsObj
|
||||
|
||||
// 智能柜扫码
|
||||
if (hardDoorId && cabinetNo) {
|
||||
router.jump("/pages/cabinet/buy", { hardDoorId, cabinetNo })
|
||||
return
|
||||
}
|
||||
// 扫收款机上生成的码支付
|
||||
if (action === "harQrCodeScanPay") {
|
||||
const { mchId, orderId } = paramsObj
|
||||
router.jump("/pages/pay/scanPay", { action, mchId, orderId })
|
||||
return
|
||||
}
|
||||
// 扫商户url收款码
|
||||
if (id) {
|
||||
router.jump("/pages/pay/scanPay", { id, name })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: "无效的二维码", icon: "none" })
|
||||
}
|
||||
|
||||
// 处理base64扫码结果
|
||||
function processBase64ScanResult(scanResult: string) {
|
||||
const decodedString = decodeBase64(scanResult)
|
||||
if (!decodedString) return
|
||||
|
||||
try {
|
||||
const decodedJson = JSON.parse(decodedString)
|
||||
if (decodedJson.id) {
|
||||
router.jump("/pages/pay/scanPay", { id: decodedJson.id })
|
||||
return
|
||||
}
|
||||
if (decodedJson?.hardDoorId && decodedJson?.cabinetNo) {
|
||||
router.jump("/pages/cabinet/buy", { hardDoorId: decodedJson.hardDoorId, cabinetNo: decodedJson.cabinetNo })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: "无效的二维码", icon: "none" })
|
||||
} catch (error) {
|
||||
uni.showToast({ title: "无效的二维码内容", icon: "none" })
|
||||
}
|
||||
}
|
||||
|
||||
// 解码base64
|
||||
function decodeBase64(base64String: string): string | undefined {
|
||||
try {
|
||||
const decodedBinaryString = atob(base64String)
|
||||
const uint8Array = new Uint8Array(decodedBinaryString.length)
|
||||
for (let i = 0; i < decodedBinaryString.length; i++) {
|
||||
uint8Array[i] = decodedBinaryString.charCodeAt(i)
|
||||
}
|
||||
const decoder = new TextDecoder("utf-8")
|
||||
const decodedString = decoder.decode(uint8Array)
|
||||
|
||||
// console.log("解码结果", decodedString)
|
||||
|
||||
return decodedString
|
||||
} catch (error) {
|
||||
console.error("Base64 解码失败:", error)
|
||||
uni.showToast({ title: "无效的二维码", icon: "none" })
|
||||
}
|
||||
}
|
||||
|
||||
return { scanCodeAction, processUrlScanResult, processBase64ScanResult }
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import { foodApi } from "../api"
|
||||
|
||||
export const useCartStore = defineStore("cartStore", {
|
||||
state: () => ({
|
||||
cart: JSON.parse(uni.getStorageSync("cart") || "[]"),
|
||||
shopMerchantId: uni.getStorageSync("shopMerchantId") || "",
|
||||
cart: JSON.parse(uni.getStorageSync("onlineShopCart") || "[]"),
|
||||
shopMerchantId: uni.getStorageSync("onlineShopMerchantId") || "",
|
||||
}),
|
||||
getters: {
|
||||
totCount(state) {
|
||||
totalCount(state) {
|
||||
return state.cart.reduce((prev: string, cur: any) => {
|
||||
return prev + cur.productCount
|
||||
}, 0)
|
||||
@@ -22,14 +22,11 @@ export const useCartStore = defineStore("cartStore", {
|
||||
actions: {
|
||||
saveShopMerchantId(shopMerchantId: string) {
|
||||
this.shopMerchantId = shopMerchantId
|
||||
uni.setStorageSync("shopMerchantId", this.shopMerchantId)
|
||||
uni.setStorageSync("onlineShopMerchantId", this.shopMerchantId)
|
||||
},
|
||||
saveToStorage() {
|
||||
uni.setStorageSync("cart", JSON.stringify(this.cart))
|
||||
},
|
||||
saveShoppingCart(cartList: any) {
|
||||
saveShopCart(cartList: any) {
|
||||
this.cart = cartList
|
||||
this.saveToStorage()
|
||||
uni.setStorageSync("onlineShopCart", JSON.stringify(this.cart))
|
||||
},
|
||||
addGoodsItemToCart(goodsItem: any) {
|
||||
const findResult = this.cart.find((item: any) => {
|
||||
@@ -40,7 +37,7 @@ export const useCartStore = defineStore("cartStore", {
|
||||
} else {
|
||||
findResult.productCount++
|
||||
}
|
||||
this.saveToStorage()
|
||||
uni.setStorageSync("onlineShopCart", JSON.stringify(this.cart))
|
||||
},
|
||||
async addProductCount(productId: string) {
|
||||
this.cart.forEach((item: any) => {
|
||||
@@ -62,7 +59,7 @@ export const useCartStore = defineStore("cartStore", {
|
||||
},
|
||||
async getShopCart() {
|
||||
const { data } = await foodApi.getShopCar({ merchantId: this.shopMerchantId, hardType: "SUPERMARKET" })
|
||||
this.saveShoppingCart(data ?? [])
|
||||
this.saveShopCart(data ?? [])
|
||||
},
|
||||
async updateShopCart() {
|
||||
const list = this.cart
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const treeToList = <T extends { children?: T[] }>(tree: T[], result: T[] = []): T[] => {
|
||||
for (const node of tree) {
|
||||
result.push(node)
|
||||
if (node?.children?.length) {
|
||||
treeToList(node.children, result)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export default treeToList
|
||||
Reference in new issue
Block a user