chore: 初始开源发布

iotlcm-app:小程序端 + 用户端服务。

- app/:uni-app 小程序前端(基于 unibest,MIT)
  - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID
  - 内网后端地址 → admin.iotlcm.example.com
  - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明)
- server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0)
  - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件)
  - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化
  - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位)
  - 依赖号码尾部的断言同步更新
  - 补 server/LICENSE(Apache-2.0)
- 移除内部资产:CI 触发器
- 补根 README,LF 行尾 + .gitattributes

已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
This commit is contained in:
MiFaZhan
2026-10-26 14:01:25 +08:00
commit 2ed5dfa6a1
1013 changed files with 138466 additions and 0 deletions
@@ -0,0 +1,293 @@
<script lang="ts" setup>
import { createWorkOrder } from '../api/workOrder'
import RouterPath from '@/constant/RouterPath'
import { useUserStore } from '@/store/user'
import { useTokenStore } from '@/store/token'
import { hasAnyFeature } from '@/utils/permission'
import { getEnvBaseUrl } from '@/utils'
definePage({
style: {
navigationBarTitleText: '提交工单',
},
})
const problemTypes = [
{ label: '卡片问题', value: 0 },
{ label: '模组问题', value: 1 },
{ label: '设备问题', value: 2 },
{ label: '订单问题', value: 3 },
{ label: '余额问题', value: 4 },
{ label: '提现问题', value: 5 },
]
const userStore = useUserStore()
const noPermission = ref(false)
const submitting = ref(false)
const uploading = ref(false)
const attachmentUrls = ref<string[]>([])
const attachmentNames = ref<string[]>([])
const canCreateWorkOrder = computed(() => hasAnyFeature(userStore.userInfo, 'workorder:create'))
const formData = reactive({
woType: [0],
title: '',
content: '',
})
const typeText = computed(() => {
const value = Number(formData.woType[0] ?? 0)
return problemTypes.find(item => item.value === value)?.label || '卡片问题'
})
function chooseAttachment() {
// #ifdef H5
const input = document.createElement('input')
input.type = 'file'
input.accept = '*/*'
input.onchange = (event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (file)
void uploadAttachment(file, file.name)
}
input.click()
return
// #endif
const choose = (uni as any).chooseFile || (uni as any).chooseMessageFile
if (typeof choose !== 'function') {
uni.showToast({ title: '当前平台暂不支持选择附件', icon: 'none' })
return
}
choose({
count: 1,
type: 'all',
success: (res: any) => {
const file = res.tempFiles?.[0] || res.tempFilePaths?.[0]
const filePath = file?.path || file?.tempFilePath || file
const fileName = file?.name || String(filePath || '').split('/').pop() || '附件'
if (filePath)
void uploadAttachment(filePath, fileName)
},
})
}
async function uploadAttachment(filePath: string | File, fileName: string) {
uploading.value = true
try {
if (typeof filePath !== 'string') {
const token = useTokenStore().updateNowTime().validToken
const form = new FormData()
form.append('file', filePath, fileName)
form.append('fileType', 'file')
const response = await fetch(`${getEnvBaseUrl()}/work-order/upload`, {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
body: form,
})
const body = await response.json()
if (!response.ok || Number(body.code) !== 0)
throw new Error(body.message || body.msg || '附件上传失败')
const data = body.data || body
const url = data.url || data.path
if (!url)
throw new Error('上传接口未返回文件地址')
attachmentUrls.value.push(url)
attachmentNames.value.push(fileName)
return
}
await new Promise<void>((resolve, reject) => {
uni.uploadFile({
url: `${getEnvBaseUrl()}/work-order/upload`,
filePath,
name: 'file',
formData: { fileType: 'file' },
success: (res) => {
try {
const body = JSON.parse(res.data)
const data = body.data || body
const url = data.url || data.path
if (!url)
throw new Error('上传接口未返回文件地址')
attachmentUrls.value.push(url)
attachmentNames.value.push(fileName)
resolve()
}
catch (error) {
reject(error)
}
},
fail: reject,
})
})
}
catch (error) {
uni.showToast({ title: getErrorMessage(error, '附件上传失败'), icon: 'none' })
}
finally {
uploading.value = false
}
}
function removeAttachment(index: number) {
attachmentUrls.value.splice(index, 1)
attachmentNames.value.splice(index, 1)
}
async function handleSubmit() {
if (!canCreateWorkOrder.value) {
uni.showToast({ title: '暂无提交工单权限', icon: 'none' })
return
}
const title = formData.title.trim()
const content = formData.content.trim()
if (!title) {
uni.showToast({ title: '请输入工单标题', icon: 'none' })
return
}
if (!content) {
uni.showToast({ title: '请输入问题描述', icon: 'none' })
return
}
submitting.value = true
try {
const res = await createWorkOrder({
woType: Number(formData.woType[0] ?? 0),
title,
content,
attachmentUrls: attachmentUrls.value,
})
const woNo = res.workOrder?.woNo || ''
uni.showToast({ title: '工单已提交', icon: 'success' })
setTimeout(() => {
if (woNo) {
uni.redirectTo({
url: `${RouterPath.BusinessViewsWorkOrderDetail}?woNo=${encodeURIComponent(woNo)}`,
})
}
else {
uni.navigateBack()
}
}, 500)
}
catch (error) {
uni.showToast({ title: getErrorMessage(error, '工单提交失败'), icon: 'none' })
}
finally {
submitting.value = false
}
}
function getErrorMessage(error: unknown, fallback: string) {
if (error && typeof error === 'object') {
const data = error as { msg?: unknown, message?: unknown, errMsg?: unknown }
if (typeof data.msg === 'string' && data.msg)
return data.msg
if (typeof data.message === 'string' && data.message)
return data.message
if (typeof data.errMsg === 'string' && data.errMsg)
return data.errMsg
}
if (error instanceof Error && error.message)
return error.message
return fallback
}
onMounted(async () => {
await userStore.fetchUserInfo().catch(() => undefined)
if (!canCreateWorkOrder.value) {
noPermission.value = true
uni.showToast({ title: '当前账号不能提交工单', icon: 'none' })
}
})
</script>
<template>
<view class="min-h-screen p-24rpx pb-40rpx">
<view class="mx-auto max-w-920rpx flex flex-col gap-20rpx">
<wd-card v-if="noPermission" custom-class="!mx-0 !mb-0">
<view class="text-26rpx text-#64748b leading-38rpx">
当前账号不能提交工单。
</view>
</wd-card>
<template v-else>
<wd-card custom-class="!mx-0 !mb-0">
<view class="flex items-start gap-16rpx">
<view class="h-64rpx w-64rpx center shrink-0 rounded-20rpx bg-#eff6ff text-#2563eb">
<view class="i-carbon-chat text-36rpx" />
</view>
<view class="min-w-0 flex-1">
<text class="block text-34rpx text-#111827 font-900">提交工单</text>
<text class="mt-8rpx block text-24rpx text-#64748b leading-36rpx">
工单会分派到平台服务部门处理
</text>
</view>
</view>
</wd-card>
<wd-form :model="formData" error-type="toast">
<wd-card custom-class="!mx-0 !mb-0" custom-content-class="!p-0">
<template #title>
<view class="h-88rpx flex items-center justify-between">
<text class="text-30rpx text-#111827 font-900">基础信息</text>
<wd-tag type="primary" variant="plain">
{{ typeText }}
</wd-tag>
</view>
</template>
<wd-cell-group :border="false">
<wd-picker
v-model="formData.woType"
label="问题类型"
label-width="96px"
:columns="problemTypes"
value-key="value"
label-key="label"
/>
<wd-input
v-model="formData.title"
label="工单标题"
label-width="96px"
placeholder="请输入简短标题"
clearable
/>
</wd-cell-group>
</wd-card>
<wd-card custom-class="!mx-0 !mb-0">
<template #title>
<view class="h-88rpx flex items-center">
<text class="text-30rpx text-#111827 font-900">问题描述</text>
</view>
</template>
<wd-textarea
v-model="formData.content"
placeholder="请描述问题现象、发生时间和已尝试的处理方式"
:maxlength="500"
show-word-limit
/>
<view class="mt-20rpx flex items-center justify-between">
<text class="text-24rpx text-#64748b">附件(可选)</text>
<wd-button size="small" type="info" :loading="uploading" @click="chooseAttachment">选择附件</wd-button>
</view>
<view v-if="attachmentNames.length" class="mt-16rpx flex flex-col gap-12rpx">
<view
v-for="(name, index) in attachmentNames"
:key="name + index"
class="flex items-center justify-between rounded-12rpx bg-#f8fafc px-16rpx py-12rpx"
>
<text class="min-w-0 flex-1 truncate text-24rpx text-#334155">{{ name }}</text>
<wd-button size="small" type="info" plain @click="removeAttachment(index)">移除</wd-button>
</view>
</view>
</wd-card>
</wd-form>
<wd-button block type="primary" :loading="submitting" :disabled="!canCreateWorkOrder || uploading" @click="handleSubmit">
提交工单
</wd-button>
</template>
</view>
</view>
</template>