refactor: 确立独立子工程架构 (backend, admin, mpuniapp) 并同步更新 AGENTS.md 规范

这个提交包含在:
wikiboss
2026-09-20 17:05:22 +08:00
父节点 de3cf8b870
当前提交 9fa1d1fbd0
共有 1679 个文件被更改,包括 204798 次插入 和 16817 次删除

查看文件

@@ -0,0 +1,34 @@
const platformInfo = process.env.uniTestPlatformInfo.toLocaleLowerCase()
const isWeb = platformInfo.startsWith('web')
const isMP = platformInfo.startsWith('mp')
const isDom2 = process.env.UNI_APP_X_DOM2 === "true"
const PAGE_PATH = '/pages/API/create-selector-query/create-selector-query-onScroll'
describe('create-selector-query-onScroll', () => {
if (isWeb || isMP || isDom2 || process.env.UNI_TEST_DEVICES_DIRECTION == 'landscape') {
it('not support', () => {
expect(1).toBe(1)
})
return
}
let page
beforeAll(async () => {
page = await program.reLaunch(PAGE_PATH)
await page.waitFor(500);
})
it('test-createSelectorQuery-onScroll', async () => {
let x = 100
let y = 250
// 滑动事件
await program.device.swipe( x, y, x, y-100, 300)
await page.waitFor(600);
const ret = await page.data('data.ret')
expect(ret).toBe(true)
})
})

查看文件

@@ -0,0 +1,50 @@
<template>
<scroll-view @scroll="onScroll" style="flex: 1;">
<view v-for="i in 60" class="content-item">
<text class="text">item-{{i}}</text>
</view>
</scroll-view>
</template>
<script setup lang="uts">
const instance = getCurrentInstance()!.proxy!
type DataType = {
ret: boolean,
}
const count = ref(0)
const data = reactive({
ret: true,
} as DataType)
function onScroll() {
count.value++
console.log("scroll: ", count.value)
uni.createSelectorQuery()
.in(instance)
.selectAll('content-item')
.boundingClientRect()
.exec((result) => {
count.value--
})
console.log("scroll: ", count.value)
if (count.value > 0) {
data.ret = false
}
}
onLoad(() => {
})
defineExpose({
data
})
</script>
<style>
</style>

查看文件

@@ -0,0 +1,168 @@
const platformInfo = process.env.uniTestPlatformInfo.toLocaleLowerCase()
const isMP = platformInfo.startsWith('mp')
const isWeb = platformInfo.startsWith('web')
const isDom2 = process.env.UNI_APP_X_DOM2 === "true"
describe('nodes-info', () => {
if (isMP) {
it('skip', () => {
expect(1).toBe(1)
})
return
}
const PAGE_PATH = '/pages/API/create-selector-query/create-selector-query'
const RECT_LEFT = 15;
const RECT_WIDTH = 150;
const RECT_HEIGHT = 100;
let page
beforeAll(async () => {
page = await program.reLaunch(PAGE_PATH)
await page.waitFor('view')
})
// 查询到的元素涉及设备精度问题,截图对比差异较大,不在截图对比,已有值断言比较
// it("screenshot", async () => {
// const image = await program.screenshot({ fullPage: true });
// expect(image).toSaveImageSnapshot();
// })
it('get-root-node-info', async () => {
// 测试 class 选择器
await getRootNode('.page')
// 测试 id 选择器
await getRootNode('#page')
// 测试 标签 选择器
// await getRootNode('page')
})
it('get-node-info', async () => {
const btnGetNodeInfo = await page.$('.btn-get-node-info')
await btnGetNodeInfo.tap()
await page.waitFor(50)
const data = await page.data('data')
// TODO 和浏览器的计算存在差异
const nodeInfo = data.nodeInfoList[0]
expect(Math.round(nodeInfo.left)).toBe(RECT_LEFT)
expect(Math.round(nodeInfo.width)).toBe(RECT_WIDTH)
expect(Math.round(nodeInfo.height)).toBe(RECT_HEIGHT)
})
it('get-all-node-info', async () => {
const btnGetAllNodeInfo = await page.$('.btn-get-all-node-info')
await btnGetAllNodeInfo.tap()
await page.waitFor(50)
const data = await page.data('data')
const nodeInfo1 = data.nodeInfoList[0]
expect(Math.round(nodeInfo1.left)).toBe(RECT_LEFT)
expect(nodeInfo1.top > 220).toBe(true)
expect(Math.round(nodeInfo1.width)).toBe(RECT_WIDTH)
expect(Math.round(nodeInfo1.height)).toBe(RECT_HEIGHT)
const nodeInfo2 = data.nodeInfoList[1]
expect(nodeInfo2.left > 180).toBe(true)
expect(nodeInfo2.top > 220).toBe(true)
expect(Math.round(nodeInfo2.width)).toBe(RECT_WIDTH)
expect(Math.round(nodeInfo2.height)).toBe(RECT_HEIGHT)
})
if(!isMP) {
// 小程序端启用了虚拟host,无法获取到子组件
it('get-node-info-child', async () => {
const child = await page.$('.node-child')
const childData = await child.data('data')
console.log('get-node-info-child.childData.top', childData.top);
expect(childData.top > 100).toBe(true)
})
}
it('multi-child', async () => {
const pageData = await page.data('data')
console.log('multi-child.pageData', pageData)
expect(pageData.selectCount).toBe(1)
expect(pageData.selectAllCount).toBe(2)
})
// #ifdef APP
//检测onResize获取BoundingClientRect信息是否有效
/* it('check_resizeRectValid', async () => {
const resizeRectValid = await page.data('resizeRectValid')
expect(resizeRectValid).toBe(true)
}) */
// #endif
if (!(isWeb || isMP)) {
it('test fields', async () => {
const pageData = await page.data('data')
expect(pageData.fieldsResultContainNode).toBe(true)
})
it('test node', async () => {
const pageData = await page.data('data')
expect(pageData.nodeResultContainNode).toBe(true)
})
}
// 测试 text 组件查询
it('get-text-node-info', async () => {
await page.callMethod('getTextNodeInfo')
await page.waitFor(100)
const textNodeInfo = await page.data('data.textNodeInfo')
expect(textNodeInfo).not.toBe(null)
expect(textNodeInfo.left).toBeCloseTo(15, 0);
expect(Math.round(textNodeInfo.right) > 300).toBe(true)
expect(Math.round(textNodeInfo.top) > 200).toBe(true)
expect(Math.round(textNodeInfo.bottom) > 200).toBe(true)
expect(Math.round(textNodeInfo.width) > 300).toBe(true)
expect(Math.round(textNodeInfo.height) > 30).toBe(true)
})
// 测试 image 组件查询
it('get-image-node-info', async () => {
await page.callMethod('getImageNodeInfo')
await page.waitFor(100)
const imageNodeInfo = await page.data('data.imageNodeInfo')
expect(imageNodeInfo).not.toBe(null)
expect(imageNodeInfo.left).toBeCloseTo(15, 0);
expect(imageNodeInfo.right).toBeCloseTo(115, 0);
expect(Math.round(imageNodeInfo.top) > 380).toBe(true)
expect(Math.round(imageNodeInfo.bottom) > 480).toBe(true)
expect(imageNodeInfo.width).toBeCloseTo(100, 0);
expect(imageNodeInfo.height).toBeCloseTo(100, 0)
})
// 测试 scroll-view 组件查询
it('get-scroll-view-node-info', async () => {
await page.callMethod('getScrollViewNodeInfo')
await page.waitFor(100)
const scrollViewNodeInfo = await page.data('data.scrollViewNodeInfo')
expect(scrollViewNodeInfo).not.toBe(null)
expect(scrollViewNodeInfo.left).toBeCloseTo(15, 0);
expect(scrollViewNodeInfo.right).toBeCloseTo(315, 0);
expect(Math.round(scrollViewNodeInfo.top) > 560).toBe(true)
expect(Math.round(scrollViewNodeInfo.bottom) > 660).toBe(true)
expect(scrollViewNodeInfo.width).toBeCloseTo(300, 0);
expect(scrollViewNodeInfo.height).toBeCloseTo(100, 0)
})
})
async function getRootNode(selector) {
const page = await program.currentPage()
await page.setData({
data:{rootNodeInfo: null,}
})
await page.waitFor(100)
await page.callMethod('getRootNodeInfo', selector)
await page.waitFor(100)
const data = await page.data('data')
expect(data.rootNodeInfo != null).toBe(true)
}

查看文件

@@ -0,0 +1,452 @@
<template>
<!-- #ifdef APP && !VUE3-VAPOR -->
<scroll-view class="page-scroll-view">
<!-- #endif -->
<view class="page uni-list-cell-db-text" style="padding-bottom: var(--uni-safe-area-inset-bottom);" id="page">
<page-head :title="data.title"></page-head>
<page-intro content="本页演示 uni.createSelectorQuery:通过 select/selectAll 与 boundingClientRect 获取节点布局信息(left、top、right、bottom、width、height);含 view、text、image、scroll-view 及子组件多根节点等查询示例,可测试 .fields/.node 结果;底部可跳转「滚动容器中的 createSelectorQuery」子页。"></page-intro>
<button class="btn btn-get-node-info" @click="getNodeInfo">getNodeInfo</button>
<button class="btn btn-get-all-node-info" @click="getAllNodeInfo">getAllNodeInfo</button>
<view id="rect-1-2" class="rect-1-2">
<view class="rect rect1"></view>
<view class="rect rect2"></view>
</view>
<view class="rect-info-1-2">
<view class="rect-info" v-for="(nodeInfo, index) in data.nodeInfoList" :key="index">
<view class="node-info-item">
<text class="node-info-item-k">left: </text>
<text class="node-info-item-v">{{nodeInfo.left}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">top: </text>
<text class="node-info-item-v">{{nodeInfo.top}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">right: </text>
<text class="node-info-item-v">{{nodeInfo.right}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">bottom: </text>
<text class="node-info-item-v">{{nodeInfo.bottom}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">width: </text>
<text class="node-info-item-v">{{nodeInfo.width}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">height: </text>
<text class="node-info-item-v">{{nodeInfo.height}}</text>
</view>
</view>
</view>
<node-child class="node-child"></node-child>
<text>子组件多根节点</text>
<multi-child ref="multiChildRef" id="multi-child"></multi-child>
<text>子组件多根节点(仅测试,用于验证查询是否超出范围)</text>
<multi-child id="multi-child-2"></multi-child>
<view>
<text>测试.fields</text>
<text>{{data.fieldsResultContainNode}}</text>
</view>
<view>
<text>测试.node</text>
<text>{{data.nodeResultContainNode}}</text>
</view>
<canvas id="canvas1"></canvas>
<!-- text 组件 -->
<view class="uni-common-mt">
<text class="uni-title-text">Text 组件查询示例</text>
<text id="test-text" class="test-text">这是一个测试文本元素</text>
<button class="btn" @click="getTextNodeInfo">查询 Text 节点信息</button>
<view v-if="data.textNodeInfo" class="rect-info">
<view class="node-info-item">
<text class="node-info-item-k">left: </text>
<text class="node-info-item-v">{{data.textNodeInfo!.left}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">right: </text>
<text class="node-info-item-v">{{data.textNodeInfo!.right}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">top: </text>
<text class="node-info-item-v">{{data.textNodeInfo!.top}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">bottom: </text>
<text class="node-info-item-v">{{data.textNodeInfo!.bottom}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">width: </text>
<text class="node-info-item-v">{{data.textNodeInfo!.width}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">height: </text>
<text class="node-info-item-v">{{data.textNodeInfo!.height}}</text>
</view>
</view>
</view>
<!-- image 组件 -->
<view class="uni-common-mt">
<text class="uni-title-text">Image 组件查询示例</text>
<image id="test-image" class="test-image" src="/static/test-image/logo.png" mode="aspectFit"></image>
<button class="btn" @click="getImageNodeInfo">查询 Image 节点信息</button>
<view v-if="data.imageNodeInfo" class="rect-info">
<view class="node-info-item">
<text class="node-info-item-k">left: </text>
<text class="node-info-item-v">{{data.imageNodeInfo!.left}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">right: </text>
<text class="node-info-item-v">{{data.imageNodeInfo!.right}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">top: </text>
<text class="node-info-item-v">{{data.imageNodeInfo!.top}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">bottom: </text>
<text class="node-info-item-v">{{data.imageNodeInfo!.bottom}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">width: </text>
<text class="node-info-item-v">{{data.imageNodeInfo!.width}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">height: </text>
<text class="node-info-item-v">{{data.imageNodeInfo!.height}}</text>
</view>
</view>
</view>
<!-- scroll-view 组件 -->
<view class="uni-common-mt">
<text class="uni-title-text">Scroll-view 组件查询示例</text>
<scroll-view id="test-scroll-view" class="test-scroll-view">
<view style="padding: 10px;">
<text v-for="i in 10" :key="i">Scroll-view 第 {{i}} 行</text>
</view>
</scroll-view>
<button class="btn" @click="getScrollViewNodeInfo">查询 Scroll-view 节点信息</button>
<view v-if="data.scrollViewNodeInfo" class="rect-info">
<view class="node-info-item">
<text class="node-info-item-k">left: </text>
<text class="node-info-item-v">{{data.scrollViewNodeInfo!.left}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">right: </text>
<text class="node-info-item-v">{{data.scrollViewNodeInfo!.right}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">top: </text>
<text class="node-info-item-v">{{data.scrollViewNodeInfo!.top}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">bottom: </text>
<text class="node-info-item-v">{{data.scrollViewNodeInfo!.bottom}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">width: </text>
<text class="node-info-item-v">{{data.scrollViewNodeInfo!.width}}</text>
</view>
<view class="node-info-item">
<text class="node-info-item-k">height: </text>
<text class="node-info-item-v">{{data.scrollViewNodeInfo!.height}}</text>
</view>
</view>
</view>
<navigator url="/pages/API/create-selector-query/create-selector-query-onScroll"><button>滚动容器中的createSelectorQuery</button></navigator>
</view>
<!-- #ifdef APP && !VUE3-VAPOR -->
</scroll-view>
<!-- #endif -->
</template>
<script setup lang="uts">
import nodeChild from './nodes-info-child.uvue'
import multiChild from './selector-query-child-multi.uvue'
type NodeInfoType = {
left : number | null,
top : number | null,
right : number | null,
bottom : number | null,
width : number | null,
height : number | null,
}
type DataType = {
title: string;
nodeInfoList: NodeInfoType[];
rootNodeInfo: NodeInfoType | null;
selectCount: number;
selectAllCount: number;
fieldsResultContainNode: boolean;
nodeResultContainNode: boolean;
textNodeInfo: NodeInfoType | null;
imageNodeInfo: NodeInfoType | null;
scrollViewNodeInfo: NodeInfoType | null;
}
// 使用reactive避免ref数据在自动化测试中无法访问
const data = reactive({
title: 'createSelectorQuery',
nodeInfoList: [],
// 仅用于自动化测试
rootNodeInfo: null,
//供自动化测试使用
// resizeRectValid: false
// TODO
selectCount: 0,
selectAllCount: 0,
fieldsResultContainNode: false,
nodeResultContainNode: false,
textNodeInfo: null,
imageNodeInfo: null,
scrollViewNodeInfo: null
} as DataType)
const multiChildRef = ref<ComponentPublicInstance | null>(null)
// 仅用于自动化测试
const onChildReady = (count: number, allCount: number) => {
// 通过事件传递数据,子组件在ready时通过事件发送数据
data.selectCount = count
data.selectAllCount = allCount
}
// 仅用于自动化测试
const getRootNodeInfo = (selector: string) => {
uni.createSelectorQuery().select(selector).boundingClientRect().exec((ret) => {
if (ret.length == 1) {
const nodeInfo = ret[0] as NodeInfo;
const nodeType = {
left: nodeInfo.left,
top: nodeInfo.top,
right: nodeInfo.right,
bottom: nodeInfo.bottom,
width: nodeInfo.width,
height: nodeInfo.height,
} as NodeInfoType;
data.rootNodeInfo = nodeType
}
})
}
const getNodeInfo = () => {
uni.createSelectorQuery().select('.rect1').boundingClientRect().exec((ret) => {
data.nodeInfoList.length = 0
const i = ret[0] as NodeInfo
data.nodeInfoList.push({
left: i.left,
top: i.top,
right: i.right,
bottom: i.bottom,
width: i.width,
height: i.height,
} as NodeInfoType)
})
}
const getAllNodeInfo = () => {
uni.createSelectorQuery().selectAll('.rect').boundingClientRect().exec((ret) => {
data.nodeInfoList.length = 0
const array = ret[0] as NodeInfo[]
array.forEach((i) => {
data.nodeInfoList.push({
left: i.left,
top: i.top,
right: i.right,
bottom: i.bottom,
width: i.width,
height: i.height,
} as NodeInfoType)
})
})
}
// test .fields
const testFields = () => {
uni.createSelectorQuery().select('.rect1').fields({
node: true
} as NodeField, (ret) => {
const isElement = (ret as NodeInfo).node instanceof UniElement
if (isElement) {
data.fieldsResultContainNode = true
} else {
data.fieldsResultContainNode = false
}
}).exec()
}
// test .node
const testNode = () => {
uni.createSelectorQuery().select('#canvas1').node((ret) => {
const isElement = (ret as NodeInfo).node instanceof UniElement
const isCanvasElement = ((ret as NodeInfo).node as UniCanvasElement).tagName == 'CANVAS'
if (isElement && isCanvasElement) {
data.nodeResultContainNode = true
} else {
data.nodeResultContainNode = false
}
}).exec()
}
// 查询 text 节点信息
const getTextNodeInfo = () => {
uni.createSelectorQuery().select('#test-text').boundingClientRect().exec((ret) => {
if (ret.length > 0) {
const i = ret[0] as NodeInfo
data.textNodeInfo = {
left: i.left,
top: i.top,
right: i.right,
bottom: i.bottom,
width: i.width,
height: i.height,
} as NodeInfoType
}
})
}
// 查询 image 节点信息
const getImageNodeInfo = () => {
uni.createSelectorQuery().select('#test-image').boundingClientRect().exec((ret) => {
if (ret.length > 0) {
const i = ret[0] as NodeInfo
data.imageNodeInfo = {
left: i.left,
top: i.top,
right: i.right,
bottom: i.bottom,
width: i.width,
height: i.height,
} as NodeInfoType
}
})
}
// 查询 scroll-view 节点信息
const getScrollViewNodeInfo = () => {
uni.createSelectorQuery().select('#test-scroll-view').boundingClientRect().exec((ret) => {
if (ret.length > 0) {
const i = ret[0] as NodeInfo
data.scrollViewNodeInfo = {
left: i.left,
top: i.top,
right: i.right,
bottom: i.bottom,
width: i.width,
height: i.height,
} as NodeInfoType
}
})
}
onLoad(() => {
uni.$on('childDataReady', onChildReady)
})
onUnload(() => {
uni.$off('childDataReady', onChildReady)
})
onReady(() => {
testFields()
testNode()
})
onResize(() => {
//供自动化测试使用
/* var rect12Element = uni.getElementById("rect-1-2")
if(rect12Element != null) {
var domRect = rect12Element.getBoundingClientRect()
if(domRect.width > 100) {
data.resizeRectValid = true
}
} */
})
defineExpose({
data,
getRootNodeInfo,
getTextNodeInfo,
getImageNodeInfo,
getScrollViewNodeInfo
})
</script>
<style>
.page {
padding: 15px;
}
.btn {
margin-top: 15px;
}
.rect-1-2 {
flex-direction: row;
margin-top: 15px;
}
.rect {
width: 150px;
height: 100px;
}
.rect1 {
background-color: dodgerblue;
}
.rect2 {
margin-left: auto;
background-color: seagreen;
}
.rect-info-1-2 {
flex-direction: row;
margin-top: 15px;
}
.rect-info {
flex: 1;
flex-direction: column;
}
.node-info-item {
flex-direction: row;
}
.node-info-item-k {
width: 72px;
line-height: 2;
}
.node-info-item-v {
font-weight: bold;
line-height: 2;
}
.test-text {
padding: 10px;
background-color: #d6d6d6;
margin: 10px 0;
font-size: 16px;
color: #333;
}
.test-image {
margin: 10px 0;
width: 100px;
height: 100px;
}
.test-scroll-view {
margin: 10px 0;
width: 300px;
height: 100px;
border: 1px solid #ccc;
}
</style>

查看文件

@@ -0,0 +1,37 @@
<template>
<view class="selector-query-child-view">
<text class="selector-query-child-text">selector-query-child</text>
</view>
</template>
<script setup lang="uts">
const instance = getCurrentInstance()!.proxy!
type DataType = {
top: number,
}
const data = reactive({
top: 0,
} as DataType)
onMounted(() => {
uni.createSelectorQuery().in(instance).select('.selector-query-child-view').boundingClientRect().exec((ret) => {
if (ret.length == 1) {
const nodeInfo = ret[0] as NodeInfo;
data.top = nodeInfo.top!
}
})
})
defineExpose({
data
})
</script>
<style>
.selector-query-child-view {
margin-top: 15px;
}
</style>

查看文件

@@ -0,0 +1,76 @@
<template>
<view class="selector-query-view">
<text>selector-query</text>
<text class="text red">{{text1}}</text>
</view>
<view class="selector-query-view">
<text>selector-query</text>
<text class="text green">{{text2}}</text>
</view>
<view v-if="text1.length>0">1</view>
<text>{{text3}}</text>
</template>
<script setup lang="uts">
const instance = getCurrentInstance()!.proxy!
const text1 = ref("")
const text2 = ref("")
const text3 = ref("test-text-node")
const viewCount = ref(0)
const selectCount = ref(0)
const selectAllCount = ref(0)
const show = ref(false)
const testCounter = ref(0)
function _dispatchEvent() {
if (testCounter.value == 2) {
// 通过事件传递数据,避免UTS无法访问动态属性的问题
uni.$emit('childDataReady', selectCount.value, selectAllCount.value)
}
}
onMounted(() => {
uni.createSelectorQuery().in(instance).select('.selector-query-view').boundingClientRect().exec((ret) => {
text1.value = JSON.stringify(ret, null, 2)
if (ret.length == 1) {
selectCount.value = ret.length
}
testCounter.value++
_dispatchEvent()
})
uni.createSelectorQuery().in(instance).selectAll('.selector-query-view').boundingClientRect().exec((ret) => {
text2.value = JSON.stringify(ret, null, 2)
if (ret.length == 1) {
selectAllCount.value = (ret[0] as NodeInfo[]).length
}
testCounter.value++
_dispatchEvent()
})
})
defineExpose({
selectCount,
selectAllCount
})
</script>
<style>
.green {
border: 3px solid green;
}
.red {
border: 3px solid red;
}
.view {
border: 3px dashed lime;
padding: 10px;
}
.text {
margin-top: 20px;
padding: 5px;
}
</style>