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

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

查看文件

@@ -0,0 +1,220 @@
// uni-app自动化测试教程: uni-app自动化测试教程: https://uniapp.dcloud.net.cn/worktile/auto/hbuilderx-extension/
describe('test-previewImage-multi', () => {
const platformInfo = process.env.uniTestPlatformInfo.toLocaleLowerCase()
const isWeb = platformInfo.startsWith('web')
const isMP = platformInfo.startsWith('mp')
const isAndroid = platformInfo.startsWith('android')
const isIos = platformInfo.startsWith('ios')
const isHarmony = platformInfo.startsWith('harmony')
const isAppWebView = process.env.UNI_AUTOMATOR_APP_WEBVIEW == 'true'
if (isWeb || isMP) {
it('other platform', () => {
expect(1).toBe(1)
})
return
}
let page;
let screenShotOptions = {};
async function screenshot() {
const image = await program.screenshot(screenShotOptions);
expect(image).toSaveImageSnapshot()
}
beforeAll(async () => {
page = await program.reLaunch('/pages/API/preview-image/preview-image-multi');
await page.waitFor(3000);
const windowInfo = await program.callUniMethod('getWindowInfo');
let topSafeArea = windowInfo.safeAreaInsets.top;
if (isAppWebView) {
if (isIos) {
topSafeArea = 59
if (platformInfo.includes('26')) {
topSafeArea = 62
}
} else if (isAndroid) {
topSafeArea = 24
windowInfo.safeArea.bottom = 867
if (platformInfo.startsWith('android 5')) {
topSafeArea = 25
}if (platformInfo.startsWith('android 6')) {
windowInfo.safeArea.bottom = 592
}if (platformInfo.startsWith('android 8')) {
windowInfo.safeArea.bottom = 534
} else if (platformInfo.startsWith('android 11')) {
topSafeArea = 52
} else if (platformInfo.startsWith('android 12')) {
topSafeArea = 24
windowInfo.safeArea.bottom = 716
} else if (platformInfo.startsWith('android 13') || platformInfo.startsWith('android 15')) {
topSafeArea = 49
windowInfo.safeArea.bottom = 891
} else if (platformInfo.startsWith('android 14')) {
windowInfo.safeArea.bottom = 891
}
} else if (isHarmony) {
topSafeArea = 39
if (platformInfo.includes('nova_12')) {
topSafeArea = 35
}
}
}
// previewImage is a full-screen overlay without the page navigation bar.
const top = topSafeArea
const bottom = windowInfo.safeArea.bottom
const left = windowInfo.safeArea.left
const right = windowInfo.safeArea.right - 10
screenShotOptions = {
deviceShot: true,
area: {
x: left,
y: top,
width: right - left,
height: bottom - top
},
}
});
it('test-previewImage-multi-1-url-number', async () => {
await page.setData({
data: {
urls: ["/static/test-image/logo.png"],
indicatorType: "number"
}
})
await page.callMethod('testPreviewImage')
await page.waitFor(1000)
await screenshot()
await page.callMethod('testClosePreviewImage')
await page.waitFor(300)
});
it('test-previewImage-multi-1-url-default', async () => {
await page.setData({
data: {
urls: ["/static/test-image/logo.png"],
indicatorType: "default"
}
})
await page.callMethod('testPreviewImage')
await page.waitFor(1000)
await screenshot()
await page.callMethod('testClosePreviewImage')
await page.waitFor(300)
});
it('test-previewImage-multi-1-url-none', async () => {
await page.setData({
data: {
urls: ["/static/test-image/logo.png"],
indicatorType: "none"
}
})
await page.callMethod('testPreviewImage')
await page.waitFor(1000)
await screenshot()
await page.callMethod('testClosePreviewImage')
await page.waitFor(300)
});
// 3图
it('test-previewImage-multi-3-url-number', async () => {
await page.setData({
data: {
urls: ["/static/test-image/logo.png", "/static/compass.png", "/static/test-image/logo.png"],
indicatorType: "number"
}
})
await page.callMethod('testPreviewImage')
await page.waitFor(1000)
await screenshot()
await page.callMethod('testClosePreviewImage')
await page.waitFor(300)
});
it('test-previewImage-multi-3-url-default', async () => {
await page.setData({
data: {
urls: ["/static/test-image/logo.png", "/static/compass.png", "/static/test-image/logo.png"],
indicatorType: "default"
}
})
await page.callMethod('testPreviewImage')
await page.waitFor(1000)
await screenshot()
await page.callMethod('testClosePreviewImage')
await page.waitFor(300)
});
it('test-previewImage-multi-3-url-none', async () => {
await page.setData({
data: {
urls: ["/static/test-image/logo.png", "/static/compass.png", "/static/test-image/logo.png"],
indicatorType: "none",
}
})
await page.callMethod('testPreviewImage')
await page.waitFor(1000)
await screenshot()
await page.callMethod('testClosePreviewImage')
await page.waitFor(300)
});
// 20 图
it('test-previewImage-multi-20-url-number', async () => {
await page.setData({
data: {
urls: ["/static/test-image/logo.png", "/static/compass.png", "/static/test-image/logo.png",
"/static/compass.png", "/static/test-image/logo.png", "/static/compass.png",
"/static/test-image/logo.png", "/static/compass.png", "/static/test-image/logo.png",
"/static/compass.png", "/static/test-image/logo.png", "/static/compass.png",
"/static/test-image/logo.png", "/static/compass.png", "/static/test-image/logo.png",
"/static/compass.png", "/static/test-image/logo.png", "/static/compass.png",
"/static/test-image/logo.png", "/static/compass.png"
],
indicatorType: "number"
}
})
await page.callMethod('testPreviewImage')
await page.waitFor(1000)
await screenshot()
await page.callMethod('testClosePreviewImage')
await page.waitFor(300)
});
it('test-previewImage-multi-20-url-default', async () => {
await page.setData({
data: {
urls: ["/static/test-image/logo.png", "/static/compass.png", "/static/test-image/logo.png",
"/static/compass.png", "/static/test-image/logo.png", "/static/compass.png",
"/static/test-image/logo.png", "/static/compass.png", "/static/test-image/logo.png",
"/static/compass.png", "/static/test-image/logo.png", "/static/compass.png",
"/static/test-image/logo.png", "/static/compass.png", "/static/test-image/logo.png",
"/static/compass.png", "/static/test-image/logo.png", "/static/compass.png",
"/static/test-image/logo.png", "/static/compass.png"
],
indicatorType: "default"
}
})
await page.callMethod('testPreviewImage')
await page.waitFor(1000)
await screenshot()
await page.callMethod('testClosePreviewImage')
await page.waitFor(300)
});
it('test-previewImage-multi-20-url-none', async () => {
await page.setData({
data: {
urls: ["/static/test-image/logo.png", "/static/compass.png", "/static/test-image/logo.png",
"/static/compass.png", "/static/test-image/logo.png", "/static/compass.png",
"/static/test-image/logo.png", "/static/compass.png", "/static/test-image/logo.png",
"/static/compass.png", "/static/test-image/logo.png", "/static/compass.png",
"/static/test-image/logo.png", "/static/compass.png", "/static/test-image/logo.png",
"/static/compass.png", "/static/test-image/logo.png", "/static/compass.png",
"/static/test-image/logo.png", "/static/compass.png"
],
indicatorType: "none"
}
})
await page.callMethod('testPreviewImage')
await page.waitFor(1000)
await screenshot()
await page.callMethod('testClosePreviewImage')
await page.waitFor(300)
});
});

查看文件

@@ -0,0 +1,39 @@
<template>
<view>
</view>
</template>
<script setup>
type Data = {
urls : Array<string>
indicatorType : string
}
const data = {
urls: [],
indicatorType: "number"
} as Data
function testPreviewImage() {
uni.previewImage({
urls: data.urls,
// #ifdef APP
indicator: data.indicatorType,
// #endif
complete() {
}
})
}
const testClosePreviewImage = () => {
uni.closePreviewImage({})
}
defineExpose({
data,
testPreviewImage,
testClosePreviewImage
})
</script>
<style>
</style>

查看文件

@@ -0,0 +1,130 @@
const platformInfo = process.env.uniTestPlatformInfo.toLocaleLowerCase()
const isWeb = platformInfo.startsWith('web')
const isMP = platformInfo.startsWith('mp')
const isAndroid = platformInfo.startsWith('android')
const isIos = platformInfo.startsWith('ios')
const isHarmony = platformInfo.startsWith('harmony')
const isAppWebView = process.env.UNI_AUTOMATOR_APP_WEBVIEW == 'true'
describe('preview-image', () => {
if ( isMP) {
it('skip', async () => {
expect(1).toBe(1);
});
return;
}
let page;
let screenShotOptions = { fullPage: true };
async function screenshot() {
const image = await program.screenshot(screenShotOptions);
expect(image).toSaveImageSnapshot()
}
beforeAll(async () => {
page = await program.reLaunch('/pages/API/preview-image/preview-image');
await page.waitFor('view');
await page.waitFor(isWeb ? 4000 : 100);
if (!isWeb) {
const windowInfo = await program.callUniMethod('getWindowInfo');
let topSafeArea = windowInfo.safeAreaInsets.top;
if (isAppWebView) {
if (isIos) {
topSafeArea = 59
if (platformInfo.includes('26')) {
topSafeArea = 62
}
} else if (isAndroid) {
topSafeArea = 24
windowInfo.safeArea.bottom = 867
if (platformInfo.startsWith('android 5')) {
topSafeArea = 25
}if (platformInfo.startsWith('android 6')) {
windowInfo.safeArea.bottom = 592
}if (platformInfo.startsWith('android 8')) {
windowInfo.safeArea.bottom = 534
} else if (platformInfo.startsWith('android 11')) {
topSafeArea = 52
} else if (platformInfo.startsWith('android 12')) {
topSafeArea = 24
windowInfo.safeArea.bottom = 716
} else if (platformInfo.startsWith('android 13') || platformInfo.startsWith('android 15')) {
topSafeArea = 49
windowInfo.safeArea.bottom = 891
} else if (platformInfo.startsWith('android 14')) {
windowInfo.safeArea.bottom = 891
}
} else if (isHarmony) {
topSafeArea = 39
if (platformInfo.includes('nova_12')) {
topSafeArea = 35
}
}
}
// previewImage is a full-screen overlay without the page navigation bar.
const top = topSafeArea
const bottom = windowInfo.safeArea.bottom
const left = windowInfo.safeArea.left
const right = windowInfo.safeArea.right - 10
screenShotOptions = {
deviceShot: true,
area: {
x: left,
y: top,
width: right - left,
height: bottom - top
},
}
}
});
if (isWeb) {
it('screenshot', async () => {
await screenshot()
});
} else {
it('previewImage_default', async () => {
await page.callMethod('previewImage')
await page.waitFor(1000)
await screenshot()
await page.callMethod('closePreviewImage')
await page.waitFor(300)
})
it('previewImage_number', async () => {
await page.callMethod('testSetCurrentIndicator','number')
await page.waitFor(300)
await page.callMethod('previewImage')
await page.waitFor(3000)
await screenshot()
await page.callMethod('closePreviewImage')
await page.waitFor(300)
})
it('previewImage_none', async () => {
await page.callMethod('testSetCurrentIndicator','none')
await page.waitFor(300)
await page.callMethod('previewImage')
await page.waitFor(3000)
await screenshot()
await page.callMethod('closePreviewImage')
await page.waitFor(300)
})
it('previewImage_longpress', async () => {
await page.callMethod('testSetCurrentIndicator','none')
await page.waitFor(300)
await page.callMethod('previewImage')
await page.waitFor(3000)
const systemInfo = await program.systemInfo()
await program.tap({
x: parseInt(systemInfo.screenWidth / 2),
y: parseInt(systemInfo.screenHeight / 2),
duration: 1000,
})
await page.waitFor(500)
await screenshot()
await page.callMethod('closePreviewImage')
await page.waitFor(300)
})
}
});

查看文件

@@ -0,0 +1,228 @@
<template>
<!-- #ifdef APP && !VUE3-VAPOR -->
<scroll-view style="flex: 1">
<!-- #endif -->
<view style="padding-left: 8px; padding-right: 8px">
<view>
<text class="text-desc">图片指示器样式</text>
<radio-group class="cell-ct" style="background-color: white" @change="onIndicatorChanged">
<view class="indicator-it" v-for="(item, index) in indicator" :key="item.value">
<radio :disabled="isWeb" :checked="index == 0" :value="item.value">{{
item.name
}}</radio>
</view>
</radio-group>
</view>
<view>
<checkbox-group @change="onCheckboxChange" style="margin-top: 16px; margin-left: 8px">
<checkbox :disabled="isWeb" :checked="isLoop" style="margin-right: 15px">循环播放</checkbox>
</checkbox-group>
</view>
<view>
<text class="text-desc">长按行为</text>
<radio-group class="cell-ct" style="background-color: white; margin-bottom: 16px;" @change="onLongPressCheckboxChange">
<view class="indicator-it" v-for="(item, index) in longPressAction" :key="item.value">
<radio :disabled="isWeb" :checked="index == 1" :value="item.value">{{
item.name
}}</radio>
</view>
</radio-group>
</view>
<view style="background-color: white">
<text class="text-desc">点击图片开始预览</text>
<view class="cell-ct" style="margin: 8px;">
<view class="cell cell-choose-image" v-for="(image, index) in imageList" :key="index">
<text style="width: 100px; height: 100px;background-color: lightgray; color: red; text-align: center; line-height: 100px;font-size: 14px;" v-if="image.error"
@click="previewImage(index)">图片路径非法</text>
<image style="width: 100px; height: 100px;background-color: white;" mode="aspectFit" :src="image.src"
v-if="!image.error" @click="previewImage(index)"
@error="onImageLoadError(index,$event as ImageErrorEvent)">
</image>
</view>
<image class="cell cell-choose-image" src="/static/plus.png" @click="chooseImage">
</image>
</view>
</view>
<view style="margin:8px;">
<text style="color: black;font-size: 18px;margin-bottom: 4px;">注意事项:</text>
<text style="font-size: 17px;margin-left: 4px;color: darkgray;">1、indicator属性仅App平台支持。</text>
<text style="font-size: 17px;margin-left: 4px;color: darkgray;">2、Web平台不支持loop属性。</text>
</view>
</view>
<!-- #ifdef APP && !VUE3-VAPOR -->
</scroll-view>
<!-- #endif -->
</template>
<script setup lang="uts">
type Indicator = "number" | "default" | "none"
type ItemType = {
value : Indicator,
name : string
}
type ImageType = {
src : string,
error : boolean
}
type LongPressType = {
value : string,
name : string
}
const imageList = ref([
{ src: "https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni@2x.png", error: false },
{ src: "/static/test-image/logo.png", error: false },
{ src: "/static/test-image/logo.svg", error: false },
// #ifdef APP
{ src: "/static/uni2.png", error: false },
// #endif
] as ImageType[])
const indicator = ref([{
value: "default",
name: "圆点"
}, {
value: "number",
name: "数字"
}, {
value: "none",
name: "不显示"
}] as ItemType[])
const longPressAction = ref([{
value: "0",
name: "默认长按行为"
}, {
value: "1",
name: "自定义长按行为"
}] as LongPressType[])
const currentIndicator = ref("default" as Indicator)
// #ifdef WEB
const isWeb = ref(true)
// #endif
// #ifndef WEB
const isWeb = ref(false)
// #endif
// #ifdef APP-IOS
const isIOS = ref(true)
// #endif
// #ifndef APP-IOS
const isIOS = ref(false)
// #endif
const isLongPress = ref(true)
const isLoop = ref(true)
const previewImage = (index : number) => {
let list = [] as Array<string>
imageList.value.forEach((item : ImageType) => {
list.push(item.src)
})
uni.previewImage({
urls: list,
current: index,
indicator: currentIndicator.value,
loop: isLoop.value,
longPressActions: (isLongPress.value ? ({
itemList: ["按钮1", "按钮2", "按钮3"],
itemColor: "#ccc",
success: (e : LongPressActionsSuccessResult) => {
uni.showToast({
title: "用户选中了第" + (e.index + 1) + "张图片,并选中了第" + (e.tapIndex + 1) + "个选项",
position: "bottom"
})
},
fail: (e : LongPressActionsFailResult) => {
uni.showToast({
title: "用户关闭了action sheet",
position: "bottom"
})
}
} as LongPressActionsOptions) : null)
})
}
const chooseImage = () => {
uni.chooseImage({
sourceType: ['album'],
count: 1,
success: (e) => {
imageList.value = imageList.value.concat({ src: e.tempFilePaths[0], error: false } as ImageType)
// imageList.value = imageList.value.concat(e.tempFilePaths)
},
fail(_) {
}
})
}
const onIndicatorChanged = (e : UniRadioGroupChangeEvent) => {
currentIndicator.value = e.detail.value as Indicator
}
const onCheckboxChange = (_ : UniCheckboxGroupChangeEvent) => {
isLoop.value = !isLoop.value
}
const onLongPressCheckboxChange = (e: UniRadioGroupChangeEvent) => {
isLongPress.value = (e.detail.value == "1")
}
const onImageLoadError = (index : number, error : UniImageErrorEvent) => {
imageList.value[index].error = true
}
const closePreviewImage = ()=>{
uni.closePreviewImage({})
uni.hideActionSheet()
}
const testSetCurrentIndicator = (value:string) =>{
currentIndicator.value = value as Indicator
}
defineExpose({
testSetCurrentIndicator,
previewImage,
closePreviewImage
})
</script>
<style>
.text-desc {
margin-top: 16px;
margin-left: 8px;
margin-bottom: 16px;
font-weight: bold;
}
.cell-ct {
display: flex;
flex-wrap: wrap;
flex-direction: row;
}
.cell {
margin-left: 3px;
margin-right: 3px;
width: 100px;
height: 100px;
}
.cell-choose-image {
border-width: 1px;
border-style: solid;
border-color: lightgray;
}
.indicator-it {
margin: 8px;
}
.cell-pd {
padding: 11px 0px;
}
</style>