Signed-off-by: 吴文峰 <kevin@lmve.net>
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
unpackage/
|
||||||
|
.hbuilderx/
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
.workbuddy/
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
onLaunch: function() {
|
||||||
|
console.log('App Launch')
|
||||||
|
},
|
||||||
|
onShow: function() {
|
||||||
|
console.log('App Show')
|
||||||
|
},
|
||||||
|
onHide: function() {
|
||||||
|
console.log('App Hide')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/*每个页面公共css */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"prompt" : "none"
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import api from './index.js'
|
||||||
|
|
||||||
|
export const customerApi = {
|
||||||
|
// 客户列表
|
||||||
|
list(params = {}) {
|
||||||
|
return api.post('/customer/list', params)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 客户详情
|
||||||
|
get(id) {
|
||||||
|
return api.post('/customer/get', { id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增客户
|
||||||
|
add(data) {
|
||||||
|
return api.post('/customer/add', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 编辑客户
|
||||||
|
update(data) {
|
||||||
|
return api.post('/customer/update', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除客户
|
||||||
|
delete(id) {
|
||||||
|
return api.post('/customer/delete', { id })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* API 请求封装
|
||||||
|
* 基于 uni.request,统一处理 Cookie 认证和错误
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useConfigStore } from '../stores/config'
|
||||||
|
|
||||||
|
// Storage Keys
|
||||||
|
const STORAGE_KEY_COOKIE_SESSION = 'userCookieSession'
|
||||||
|
|
||||||
|
// 错误码定义
|
||||||
|
const ERR_COOKIE_EXPIRED = 'userCookieError'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 API 基础地址
|
||||||
|
*/
|
||||||
|
function getBaseUrl() {
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
return configStore.getApiBaseUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取保存的 Cookie 值
|
||||||
|
*/
|
||||||
|
function getCookie() {
|
||||||
|
try {
|
||||||
|
// 优先从 storage 读取(避免 Pinia 初始化时序问题)
|
||||||
|
const cookieStr = uni.getStorageSync(STORAGE_KEY_COOKIE_SESSION)
|
||||||
|
if (cookieStr) {
|
||||||
|
const cookie = JSON.parse(cookieStr)
|
||||||
|
return cookie?.Value ?? ''
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求封装 - 统一错误处理
|
||||||
|
*/
|
||||||
|
function request(options) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const baseUrl = getBaseUrl()
|
||||||
|
if (!baseUrl) {
|
||||||
|
reject({ errCode: -1, message: 'API地址未配置' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET 请求不注入 cookie,POST 请求注入 cookie
|
||||||
|
let data = options.data || {}
|
||||||
|
if (options.method === 'POST') {
|
||||||
|
const cookie = getCookie()
|
||||||
|
// 后端期望格式: { data: {...业务数据}, userCookieValue: "xxx" }
|
||||||
|
data = {
|
||||||
|
data: data, // 业务数据包装在 data 字段
|
||||||
|
userCookieValue: cookie || undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.request({
|
||||||
|
url: baseUrl + options.path,
|
||||||
|
method: options.method || 'GET',
|
||||||
|
data,
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
timeout: options.timeout || 10000,
|
||||||
|
success: (res) => {
|
||||||
|
const data = res.data
|
||||||
|
|
||||||
|
// 检查 Cookie 过期
|
||||||
|
if (data?.err_code === ERR_COOKIE_EXPIRED || data?.err_code === -44) {
|
||||||
|
// 清除存储的 Cookie
|
||||||
|
uni.removeStorageSync(STORAGE_KEY_COOKIE_SESSION)
|
||||||
|
uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })
|
||||||
|
reject({ errCode: -44, message: '登录已过期' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
resolve({
|
||||||
|
errCode: data?.err_code === 0 ? 0 : -1,
|
||||||
|
data: data?.return || null,
|
||||||
|
raw: data
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '服务异常', icon: 'none' })
|
||||||
|
reject({ errCode: -1, message: '服务异常' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
uni.showToast({ title: '网络错误', icon: 'none' })
|
||||||
|
reject({ errCode: -2, message: '网络错误', detail: err })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 接口汇总
|
||||||
|
*/
|
||||||
|
export const api = {
|
||||||
|
/**
|
||||||
|
* GET 请求(不需要认证)
|
||||||
|
*/
|
||||||
|
get(path, data = {}) {
|
||||||
|
return request({
|
||||||
|
path,
|
||||||
|
method: 'GET',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST JSON 请求(需要认证)
|
||||||
|
*/
|
||||||
|
post(path, data = {}) {
|
||||||
|
return request({
|
||||||
|
path,
|
||||||
|
method: 'POST',
|
||||||
|
data // 业务数据,会被包装成 { data, userCookieValue }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST FormData(文件上传)
|
||||||
|
*/
|
||||||
|
upload(path, fileData) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const baseUrl = getBaseUrl()
|
||||||
|
const cookie = getCookie()
|
||||||
|
|
||||||
|
uni.uploadFile({
|
||||||
|
url: baseUrl + path,
|
||||||
|
filePath: fileData.uri,
|
||||||
|
name: fileData.name || 'file',
|
||||||
|
formData: {
|
||||||
|
cookie: cookie || ''
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(res.data)
|
||||||
|
resolve({
|
||||||
|
errCode: data?.err_code === 0 ? 0 : -1,
|
||||||
|
data: data?.return || null,
|
||||||
|
raw: data
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
reject({ errCode: -1, message: '解析响应失败' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
reject({ errCode: -2, message: '上传失败', detail: err })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default api
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* 采购订单 API
|
||||||
|
*/
|
||||||
|
import api from './index.js'
|
||||||
|
|
||||||
|
export const purchaseApi = {
|
||||||
|
// 获取订单列表
|
||||||
|
getOrders(data = {}) {
|
||||||
|
return api.post('/purchase/getorders', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取订单详情
|
||||||
|
getOrder(data = {}) {
|
||||||
|
return api.post('/purchase/getorder', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取订单数量统计
|
||||||
|
getOrderCount() {
|
||||||
|
return api.post('/purchase/getordercount', {})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新订单状态
|
||||||
|
updateOrderStatus(data = {}) {
|
||||||
|
return api.post('/purchase/updatestatus', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增订单
|
||||||
|
addOrder(data = {}) {
|
||||||
|
return api.post('/purchase/addorder', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新订单
|
||||||
|
updateOrder(data = {}) {
|
||||||
|
return api.post('/purchase/updateorder', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default purchaseApi
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { useConfigStore } from '@/stores/config'
|
||||||
|
|
||||||
|
const useConfig=useConfigStore()
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 日程管理 API
|
||||||
|
*/
|
||||||
|
import api from './index.js'
|
||||||
|
|
||||||
|
export const scheduleApi = {
|
||||||
|
// 获取日程列表
|
||||||
|
getEvents(data = {}) {
|
||||||
|
return api.post('/schedule/getevents', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增日程
|
||||||
|
addEvent(data = {}) {
|
||||||
|
return api.post('/schedule/addevent', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 编辑日程
|
||||||
|
editEvent(data = {}) {
|
||||||
|
return api.post('/schedule/editevent', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除日程
|
||||||
|
deleteEvent(data = {}) {
|
||||||
|
return api.post('/schedule/deleevent', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default scheduleApi
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* 用户相关 API
|
||||||
|
*/
|
||||||
|
import api from './index'
|
||||||
|
|
||||||
|
export const userApi = {
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
* @param {string} username - 用户名
|
||||||
|
* @param {string} password - 密码
|
||||||
|
* @param {boolean} remember - 是否记住登录
|
||||||
|
* @returns {Promise<{errCode, data: {cookie}}>}
|
||||||
|
*/
|
||||||
|
login(username, password, remember = true) {
|
||||||
|
return api.post('/users/login', {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
remember
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户注册
|
||||||
|
* @param {string} username - 用户名
|
||||||
|
* @param {string} email - 邮箱
|
||||||
|
* @param {string} password - 密码
|
||||||
|
*/
|
||||||
|
register(username, email, password) {
|
||||||
|
return api.post('/users/register', {
|
||||||
|
username,
|
||||||
|
useremail: email,
|
||||||
|
userpass: password
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户信息
|
||||||
|
* @returns {Promise<{errCode, data: {user, userInfo}}>}
|
||||||
|
*/
|
||||||
|
getUserInfo() {
|
||||||
|
return api.post('/users/getinfo', {})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
* @param {string} oldPass - 旧密码
|
||||||
|
* @param {string} newPass - 新密码
|
||||||
|
*/
|
||||||
|
changePassword(oldPass, newPass) {
|
||||||
|
return api.post('/users/changePassword', {
|
||||||
|
oldpass: oldPass,
|
||||||
|
newpass: newPass
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改邮箱
|
||||||
|
* @param {string} newEmail - 新邮箱
|
||||||
|
*/
|
||||||
|
changeEmail(newEmail) {
|
||||||
|
return api.post('/users/changeEmail', {
|
||||||
|
newemail: newEmail
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
* @param {Object} data - 用户信息 { username, remark, birthday }
|
||||||
|
*/
|
||||||
|
updateInfo(data) {
|
||||||
|
return api.post('/users/updateInfo', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传头像
|
||||||
|
* @param {string} fileUri - 文件本地路径
|
||||||
|
*/
|
||||||
|
updateAvatar(fileUri) {
|
||||||
|
return api.upload('/users/updateAvatar', {
|
||||||
|
name: 'file',
|
||||||
|
uri: fileUri
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default userApi
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 用户信息 API
|
||||||
|
*/
|
||||||
|
import api from './index.js'
|
||||||
|
|
||||||
|
export const usersApi = {
|
||||||
|
// 通过用户ID获取用户信息(GET 请求)
|
||||||
|
getUserInfoFromUserID(userID) {
|
||||||
|
return api.get('/users/getuserinfo/' + userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usersApi
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import api from './index.js'
|
||||||
|
|
||||||
|
export const warehouseApi = {
|
||||||
|
// 获取容器列表
|
||||||
|
listContainer(params = {}) {
|
||||||
|
return api.post('/warehouse/list_container', params)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取容器详情
|
||||||
|
getContainer(id) {
|
||||||
|
return api.post('/warehouse/get_container', { id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增容器
|
||||||
|
addContainer(data) {
|
||||||
|
return api.post('/warehouse/add_container', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新容器
|
||||||
|
updateContainer(data) {
|
||||||
|
return api.post('/warehouse/update_container', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除容器
|
||||||
|
deleteContainer(id) {
|
||||||
|
return api.post('/warehouse/delete_container', { id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取物品列表
|
||||||
|
listItem(params = {}) {
|
||||||
|
return api.post('/warehouse/list_item', params)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取物品详情
|
||||||
|
getItem(id) {
|
||||||
|
return api.post('/warehouse/get_item', { id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增物品
|
||||||
|
addItem(data) {
|
||||||
|
return api.post('/warehouse/add_item', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新物品
|
||||||
|
updateItem(data) {
|
||||||
|
return api.post('/warehouse/update_item', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除物品
|
||||||
|
deleteItem(id) {
|
||||||
|
return api.post('/warehouse/delete_item', { id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 移动物品
|
||||||
|
moveItem(id, containerId) {
|
||||||
|
return api.post('/warehouse/move_item', {
|
||||||
|
item_id: id,
|
||||||
|
new_container: containerId
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取仓库统计
|
||||||
|
getStats() {
|
||||||
|
return api.post('/warehouse/get_stats', {})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import api from './index.js'
|
||||||
|
|
||||||
|
export const workOrderApi = {
|
||||||
|
// 获取工单列表
|
||||||
|
list(params = {}) {
|
||||||
|
return api.post('/work_order/list', params)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取工单详情
|
||||||
|
get(id) {
|
||||||
|
return api.post('/work_order/get', { id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取工单统计
|
||||||
|
getCount() {
|
||||||
|
return api.post('/work_order/count', {})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增工单
|
||||||
|
add(data) {
|
||||||
|
return api.post('/work_order/add', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新工单
|
||||||
|
update(data) {
|
||||||
|
return api.post('/work_order/update', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除工单
|
||||||
|
delete(id) {
|
||||||
|
return api.post('/work_order/delete', { id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 提交进度
|
||||||
|
commit(data) {
|
||||||
|
return api.post('/work_order/commit', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 搜索采购订单
|
||||||
|
searchPurchaseOrders(query, limit = 10) {
|
||||||
|
return api.post('/work_order/search_purchase_orders', { search: query, limit })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* 打包前自动递增版本号
|
||||||
|
* 用法: node bump-version.js
|
||||||
|
*
|
||||||
|
* - versionCode: 每次 +1(必须递增,否则 Android 无法覆盖安装)
|
||||||
|
* - versionName: 语义化版本自动递增 patch 位(1.0.0 → 1.0.1)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const manifestPath = path.join(__dirname, 'manifest.json')
|
||||||
|
|
||||||
|
if (!fs.existsSync(manifestPath)) {
|
||||||
|
console.error('未找到 manifest.json,请在 ops2_uniapp 目录下运行此脚本')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// manifest.json 可能包含 JS 风格注释,先去掉再解析
|
||||||
|
const raw = fs.readFileSync(manifestPath, 'utf8')
|
||||||
|
const cleaned = raw.replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, '')
|
||||||
|
const manifest = JSON.parse(cleaned)
|
||||||
|
|
||||||
|
// 递增 versionCode
|
||||||
|
const oldCode = parseInt(manifest.versionCode) || 0
|
||||||
|
const newCode = oldCode + 1
|
||||||
|
manifest.versionCode = newCode.toString()
|
||||||
|
|
||||||
|
// 递增 versionName 的 patch 位
|
||||||
|
const parts = (manifest.versionName || '1.0.0').split('.')
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
parts[2] = (parseInt(parts[2]) + 1).toString()
|
||||||
|
} else {
|
||||||
|
parts.push('1')
|
||||||
|
}
|
||||||
|
manifest.versionName = parts.join('.')
|
||||||
|
|
||||||
|
// 写回
|
||||||
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
|
||||||
|
console.log(`版本已更新: ${manifest.versionName} (${manifest.versionCode})`)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<view v-if="visible" class="my-toast" :class="typeClass">
|
||||||
|
<text>{{ message }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const message = ref('')
|
||||||
|
const type = ref('info') // info | success | error | warning
|
||||||
|
|
||||||
|
const typeClass = computed(() => `toast-${type.value}`)
|
||||||
|
|
||||||
|
// 暴露方法给外部调用
|
||||||
|
function show(msg, t = 'info', duration = 2000) {
|
||||||
|
message.value = msg
|
||||||
|
type.value = t
|
||||||
|
visible.value = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
visible.value = false
|
||||||
|
}, duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快捷方法
|
||||||
|
function success(msg, duration = 2000) { show(msg, 'success', duration) }
|
||||||
|
function error(msg, duration = 2000) { show(msg, 'error', duration) }
|
||||||
|
function warning(msg, duration = 2000) { show(msg, 'warning', duration) }
|
||||||
|
function info(msg, duration = 2000) { show(msg, 'info', duration) }
|
||||||
|
|
||||||
|
defineExpose({ show, success, error, warning, info })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 固定顶部居中 */
|
||||||
|
.my-toast {
|
||||||
|
position: fixed;
|
||||||
|
top: 120rpx;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 20rpx 40rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
z-index: 9999;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 600rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 四种类型 */
|
||||||
|
.toast-info {
|
||||||
|
background-color: #007AFF;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-success {
|
||||||
|
background-color: #34C759;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-error {
|
||||||
|
background-color: #FF3B30;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-warning {
|
||||||
|
background-color: #FF9500;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script>
|
||||||
|
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||||
|
CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||||
|
</script>
|
||||||
|
<title></title>
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import App from './App'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
// #ifndef VUE3
|
||||||
|
import Vue from 'vue'
|
||||||
|
import './uni.promisify.adaptor'
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
App.mpType = 'app'
|
||||||
|
const app = new Vue({
|
||||||
|
...App
|
||||||
|
})
|
||||||
|
app.$mount()
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef VUE3
|
||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
const pinia = createPinia()
|
||||||
|
app.use(pinia)
|
||||||
|
return {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
{
|
||||||
|
"name" : "Operations",
|
||||||
|
"appid" : "__UNI__8A0DE5E",
|
||||||
|
"description" : "Operations(运营)的缩写,一个前后端分离的工作流/运营管理系统。",
|
||||||
|
"versionName" : "1.4.3",
|
||||||
|
"versionCode" : "143",
|
||||||
|
"transformPx" : false,
|
||||||
|
"app-plus" : {
|
||||||
|
"usingComponents" : true,
|
||||||
|
"nvueStyleCompiler" : "uni-app",
|
||||||
|
"compilerVersion" : 3,
|
||||||
|
"splashscreen" : {
|
||||||
|
"alwaysShowBeforeRender" : true,
|
||||||
|
"waiting" : true,
|
||||||
|
"autoclose" : true,
|
||||||
|
"delay" : 0
|
||||||
|
},
|
||||||
|
"modules" : {
|
||||||
|
"Barcode" : {},
|
||||||
|
"Bluetooth" : {},
|
||||||
|
"Camera" : {}
|
||||||
|
},
|
||||||
|
"distribute" : {
|
||||||
|
"android" : {
|
||||||
|
"permissions" : [
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ios" : {
|
||||||
|
"dSYMs" : false
|
||||||
|
},
|
||||||
|
"sdkConfigs" : {},
|
||||||
|
"icons" : {
|
||||||
|
"android" : {
|
||||||
|
"hdpi" : "unpackage/res/icons/72x72.png",
|
||||||
|
"xhdpi" : "unpackage/res/icons/96x96.png",
|
||||||
|
"xxhdpi" : "unpackage/res/icons/144x144.png",
|
||||||
|
"xxxhdpi" : "unpackage/res/icons/192x192.png"
|
||||||
|
},
|
||||||
|
"ios" : {
|
||||||
|
"appstore" : "unpackage/res/icons/1024x1024.png",
|
||||||
|
"ipad" : {
|
||||||
|
"app" : "unpackage/res/icons/76x76.png",
|
||||||
|
"app@2x" : "unpackage/res/icons/152x152.png",
|
||||||
|
"notification" : "unpackage/res/icons/20x20.png",
|
||||||
|
"notification@2x" : "unpackage/res/icons/40x40.png",
|
||||||
|
"proapp@2x" : "unpackage/res/icons/167x167.png",
|
||||||
|
"settings" : "unpackage/res/icons/29x29.png",
|
||||||
|
"settings@2x" : "unpackage/res/icons/58x58.png",
|
||||||
|
"spotlight" : "unpackage/res/icons/40x40.png",
|
||||||
|
"spotlight@2x" : "unpackage/res/icons/80x80.png"
|
||||||
|
},
|
||||||
|
"iphone" : {
|
||||||
|
"app@2x" : "unpackage/res/icons/120x120.png",
|
||||||
|
"app@3x" : "unpackage/res/icons/180x180.png",
|
||||||
|
"notification@2x" : "unpackage/res/icons/40x40.png",
|
||||||
|
"notification@3x" : "unpackage/res/icons/60x60.png",
|
||||||
|
"settings@2x" : "unpackage/res/icons/58x58.png",
|
||||||
|
"settings@3x" : "unpackage/res/icons/87x87.png",
|
||||||
|
"spotlight@2x" : "unpackage/res/icons/80x80.png",
|
||||||
|
"spotlight@3x" : "unpackage/res/icons/120x120.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"splashscreen" : {
|
||||||
|
"useOriginalMsgbox" : false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nativePlugins" : {
|
||||||
|
"LcPrinter" : {
|
||||||
|
"__plugin_info__" : {
|
||||||
|
"name" : "LcPrinter",
|
||||||
|
"description" : "打印插件",
|
||||||
|
"platforms" : "Android",
|
||||||
|
"url" : "",
|
||||||
|
"android_package_name" : "",
|
||||||
|
"ios_bundle_id" : "",
|
||||||
|
"isCloud" : false,
|
||||||
|
"bought" : -1,
|
||||||
|
"pid" : "",
|
||||||
|
"parameters" : {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quickapp" : {},
|
||||||
|
"mp-weixin" : {
|
||||||
|
"appid" : "",
|
||||||
|
"setting" : {
|
||||||
|
"urlCheck" : false
|
||||||
|
},
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-alipay" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-baidu" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-toutiao" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"uniStatistics" : {
|
||||||
|
"enable" : false
|
||||||
|
},
|
||||||
|
"vueVersion" : "3",
|
||||||
|
"h5" : {
|
||||||
|
"devServer" : {
|
||||||
|
"port" : 5174
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"locale" : "auto"
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "LcPrinter",
|
||||||
|
"id": "LcPrinter",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"description": "打印插件",
|
||||||
|
"_dp_type":"nativeplugin",
|
||||||
|
"_dp_nativeplugin":{
|
||||||
|
"android": {
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"name": "LcPrinter",
|
||||||
|
"class": "uni.dcloud.io.uniplugin_lcprint.PrinterModule"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"abis": [
|
||||||
|
"armeabi-v7a",
|
||||||
|
"arm64-v8a",
|
||||||
|
"x86"
|
||||||
|
],
|
||||||
|
"integrateType": "aar",
|
||||||
|
"minSdkVersion" : 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
{
|
||||||
|
"name": "ops2_uniapp",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"pinia": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-string-parser": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
|
"version": "7.28.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||||
|
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/parser": {
|
||||||
|
"version": "7.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
|
||||||
|
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.29.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"parser": "bin/babel-parser.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/types": {
|
||||||
|
"version": "7.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||||
|
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
|
"@babel/helper-validator-identifier": "^7.28.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-core": {
|
||||||
|
"version": "3.5.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz",
|
||||||
|
"integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.29.2",
|
||||||
|
"@vue/shared": "3.5.32",
|
||||||
|
"entities": "^7.0.1",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-dom": {
|
||||||
|
"version": "3.5.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz",
|
||||||
|
"integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-core": "3.5.32",
|
||||||
|
"@vue/shared": "3.5.32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-sfc": {
|
||||||
|
"version": "3.5.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
|
||||||
|
"integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.29.2",
|
||||||
|
"@vue/compiler-core": "3.5.32",
|
||||||
|
"@vue/compiler-dom": "3.5.32",
|
||||||
|
"@vue/compiler-ssr": "3.5.32",
|
||||||
|
"@vue/shared": "3.5.32",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"postcss": "^8.5.8",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-ssr": {
|
||||||
|
"version": "3.5.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz",
|
||||||
|
"integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-dom": "3.5.32",
|
||||||
|
"@vue/shared": "3.5.32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/devtools-api": {
|
||||||
|
"version": "7.7.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
|
||||||
|
"integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-kit": "^7.7.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/devtools-kit": {
|
||||||
|
"version": "7.7.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
|
||||||
|
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-shared": "^7.7.9",
|
||||||
|
"birpc": "^2.3.0",
|
||||||
|
"hookable": "^5.5.3",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"perfect-debounce": "^1.0.0",
|
||||||
|
"speakingurl": "^14.0.1",
|
||||||
|
"superjson": "^2.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/devtools-shared": {
|
||||||
|
"version": "7.7.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
|
||||||
|
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"rfdc": "^1.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/reactivity": {
|
||||||
|
"version": "3.5.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
|
||||||
|
"integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/shared": "3.5.32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/runtime-core": {
|
||||||
|
"version": "3.5.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz",
|
||||||
|
"integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "3.5.32",
|
||||||
|
"@vue/shared": "3.5.32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/runtime-dom": {
|
||||||
|
"version": "3.5.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz",
|
||||||
|
"integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "3.5.32",
|
||||||
|
"@vue/runtime-core": "3.5.32",
|
||||||
|
"@vue/shared": "3.5.32",
|
||||||
|
"csstype": "^3.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/server-renderer": {
|
||||||
|
"version": "3.5.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz",
|
||||||
|
"integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-ssr": "3.5.32",
|
||||||
|
"@vue/shared": "3.5.32"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "3.5.32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/shared": {
|
||||||
|
"version": "3.5.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz",
|
||||||
|
"integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/birpc": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/copy-anything": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-what": "^5.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/hookable": {
|
||||||
|
"version": "5.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||||
|
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/is-what": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/magic-string": {
|
||||||
|
"version": "0.30.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mitt": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/perfect-debounce": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/pinia": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^7.7.7"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.5.0",
|
||||||
|
"vue": "^3.5.11"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||||
|
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.11",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rfdc": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/speakingurl": {
|
||||||
|
"version": "14.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||||
|
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/superjson": {
|
||||||
|
"version": "2.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
|
||||||
|
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"copy-anything": "^4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue": {
|
||||||
|
"version": "3.5.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
|
||||||
|
"integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-dom": "3.5.32",
|
||||||
|
"@vue/compiler-sfc": "3.5.32",
|
||||||
|
"@vue/runtime-dom": "3.5.32",
|
||||||
|
"@vue/server-renderer": "3.5.32",
|
||||||
|
"@vue/shared": "3.5.32"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"pinia": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/index/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "主页"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/order/order",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "订单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/order/order-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "订单详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/order/order-add",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "新增订单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/order/edit-order",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "编辑订单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/workorder/workorder",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "工单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/workorder/add-workorder",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "新建工单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/workorder/show-workorder",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "工单详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/workorder/edit-workorder",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "编辑工单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/warehouse/warehouse",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "仓库"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/warehouse/item-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "物品详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/warehouse/add-item",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "新增物品"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/warehouse/item-edit",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "编辑物品"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/user/user",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "用户"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/login",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "登录"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/settings/settings",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "设置"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/printer-test/printer-test",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "打印机测试"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/message/message",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "消息"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/search/search",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "搜索"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarTitleText": "uni-app",
|
||||||
|
"navigationBarBackgroundColor": "#F8F8F8",
|
||||||
|
"backgroundColor": "#F8F8F8",
|
||||||
|
"usingComponents": {
|
||||||
|
"my-toast": "/components/my-toast/my-toast"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tabBar": {
|
||||||
|
"color": "#7A7E83",
|
||||||
|
"selectedColor": "#007AFF",
|
||||||
|
"borderStyle": "black",
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"pagePath": "pages/index/index",
|
||||||
|
"iconPath": "static/tabbar/home.png",
|
||||||
|
"selectedIconPath": "static/tabbar/home-active.png",
|
||||||
|
"text": "主页"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/order/order",
|
||||||
|
"iconPath": "static/tabbar/order.png",
|
||||||
|
"selectedIconPath": "static/tabbar/order-active.png",
|
||||||
|
"text": "订单"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/workorder/workorder",
|
||||||
|
"iconPath": "static/tabbar/workorder.png",
|
||||||
|
"selectedIconPath": "static/tabbar/workorder-active.png",
|
||||||
|
"text": "工单"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/warehouse/warehouse",
|
||||||
|
"iconPath": "static/tabbar/warehouse.png",
|
||||||
|
"selectedIconPath": "static/tabbar/warehouse-active.png",
|
||||||
|
"text": "仓库"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/user/user",
|
||||||
|
"iconPath": "static/tabbar/user.png",
|
||||||
|
"selectedIconPath": "static/tabbar/user-active.png",
|
||||||
|
"text": "用户"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uniIdRouter": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 欢迎区域 -->
|
||||||
|
<view class="welcome-section">
|
||||||
|
<view class="welcome-header">
|
||||||
|
<text class="welcome-title">{{ welcomeText }}</text>
|
||||||
|
<text class="search-icon" @click="goSearch">🔍</text>
|
||||||
|
</view>
|
||||||
|
<text class="welcome-date">{{ todayDisplay }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 今日日程 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="card-title">📅 今日日程</text>
|
||||||
|
<text class="card-count" v-if="todaySchedules.length > 0">{{ todaySchedules.length }}个</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view v-if="loadingSchedules" class="loading">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 日程列表 -->
|
||||||
|
<view v-else-if="todaySchedules.length > 0" class="schedule-list">
|
||||||
|
<view
|
||||||
|
v-for="schedule in todaySchedules"
|
||||||
|
:key="schedule.ID"
|
||||||
|
class="schedule-item"
|
||||||
|
>
|
||||||
|
<view class="schedule-date" :style="{ backgroundColor: schedule.BgColor || '#007AFF' }">
|
||||||
|
<text>{{ formatScheduleDate(schedule) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="schedule-content">
|
||||||
|
<text class="schedule-title">{{ schedule.Title }}</text>
|
||||||
|
<text class="schedule-user">创建人: {{ getCreatorName(schedule.UserID) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 无日程 -->
|
||||||
|
<view v-else class="empty">
|
||||||
|
<text class="empty-text">今日暂无日程</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 订单统计 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="card-title">📦 待处理订单</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view v-if="loadingOrders" class="loading">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 订单数量 -->
|
||||||
|
<view v-else class="order-stats">
|
||||||
|
<view class="stat-item" @click="switchToTab('/pages/order/order')">
|
||||||
|
<text class="stat-num" :class="{ 'stat-warning': pendingCount > 0 }">{{ pendingCount || '—' }}</text>
|
||||||
|
<text class="stat-label">待处理</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item" @click="switchToTab('/pages/order/order')">
|
||||||
|
<text class="stat-num">{{ arrivedCount || '—' }}</text>
|
||||||
|
<text class="stat-label">已到达</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item" @click="switchToTab('/pages/order/order')">
|
||||||
|
<text class="stat-num">{{ receivedCount || '—' }}</text>
|
||||||
|
<text class="stat-label">已收件</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 功能入口 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="card-title">🚀 快捷入口</text>
|
||||||
|
</view>
|
||||||
|
<view class="quick-links">
|
||||||
|
<view class="quick-item" @click="switchToTab('/pages/order/order')">
|
||||||
|
<text class="quick-icon">📦</text>
|
||||||
|
<text class="quick-text">订单管理</text>
|
||||||
|
</view>
|
||||||
|
<view class="quick-item" @click="switchToTab('/pages/warehouse/warehouse')">
|
||||||
|
<text class="quick-icon">🏭</text>
|
||||||
|
<text class="quick-text">仓库管理</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onShow, onHide } from '@dcloudio/uni-app'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useUserStore } from '../../stores/user'
|
||||||
|
import { scheduleApi } from '../../api/schedule'
|
||||||
|
import { purchaseApi } from '../../api/purchase'
|
||||||
|
import { fetchUserInfo, getUsername } from '../../stores/users'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 今日日期
|
||||||
|
const today = new Date()
|
||||||
|
const todayStr = today.toISOString().split('T')[0]
|
||||||
|
|
||||||
|
// 欢迎语
|
||||||
|
const welcomeText = computed(() => {
|
||||||
|
if (userStore.isLoggedIn) {
|
||||||
|
return `${userStore.username || '用户'},您好!`
|
||||||
|
}
|
||||||
|
return '欢迎使用 OPS 系统'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 今日日期显示
|
||||||
|
const todayDisplay = computed(() => {
|
||||||
|
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||||
|
return `${today.getMonth() + 1}月${today.getDate()}日 ${weekdays[today.getDay()]}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 日程数据
|
||||||
|
const todaySchedules = ref([])
|
||||||
|
const loadingSchedules = ref(false)
|
||||||
|
|
||||||
|
// 订单数据
|
||||||
|
const pendingCount = ref(0)
|
||||||
|
const arrivedCount = ref(0)
|
||||||
|
const receivedCount = ref(0)
|
||||||
|
const loadingOrders = ref(false)
|
||||||
|
|
||||||
|
// 获取今日日程
|
||||||
|
async function fetchTodaySchedules(silent = false) {
|
||||||
|
if (!silent) loadingSchedules.value = true
|
||||||
|
try {
|
||||||
|
const res = await scheduleApi.getEvents({
|
||||||
|
start: todayStr,
|
||||||
|
end: todayStr
|
||||||
|
})
|
||||||
|
if (res.errCode === 0 && res.data?.list) {
|
||||||
|
todaySchedules.value = res.data.list
|
||||||
|
// 预加载所有创建人的用户名
|
||||||
|
const userIDs = [...new Set(res.data.list.map(s => s.UserID).filter(Boolean))]
|
||||||
|
userIDs.forEach(userID => fetchUserInfo(userID))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取今日日程失败', e)
|
||||||
|
} finally {
|
||||||
|
if (!silent) loadingSchedules.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单统计
|
||||||
|
async function fetchOrderStats(silent = false) {
|
||||||
|
if (!silent) loadingOrders.value = true
|
||||||
|
try {
|
||||||
|
const res = await purchaseApi.getOrderCount()
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
pendingCount.value = res.data.pending || 0
|
||||||
|
arrivedCount.value = res.data.arrived || 0
|
||||||
|
receivedCount.value = res.data.received || 0
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取订单统计失败', e)
|
||||||
|
} finally {
|
||||||
|
if (!silent) loadingOrders.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日程日期
|
||||||
|
function formatScheduleDate(schedule) {
|
||||||
|
if (schedule.StartDate !== schedule.EndDate) {
|
||||||
|
return `${schedule.StartDate}\n至\n${schedule.EndDate}`
|
||||||
|
}
|
||||||
|
return schedule.StartDate
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取创建人名称(使用用户缓存)
|
||||||
|
function getCreatorName(userId) {
|
||||||
|
if (!userId) return '未知'
|
||||||
|
// 如果是当前用户,显示"我"
|
||||||
|
if (userStore.userInfo && userStore.userInfo.ID === userId) {
|
||||||
|
return userStore.username || '我'
|
||||||
|
}
|
||||||
|
// 从缓存获取用户名
|
||||||
|
return getUsername(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到 TabBar 页面
|
||||||
|
function switchToTab(url) {
|
||||||
|
uni.switchTab({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goSearch() {
|
||||||
|
uni.navigateTo({ url: '/pages/search/search' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定时刷新
|
||||||
|
let refreshTimer = null
|
||||||
|
const REFRESH_INTERVAL = 5000 // 5秒
|
||||||
|
|
||||||
|
// 开始定时刷新(静默刷新,不显示 loading)
|
||||||
|
function startRefreshTimer() {
|
||||||
|
stopRefreshTimer()
|
||||||
|
refreshTimer = setInterval(() => {
|
||||||
|
fetchTodaySchedules(true) // true = 静默刷新
|
||||||
|
if (userStore.isLoggedIn) {
|
||||||
|
fetchOrderStats(true) // true = 静默刷新
|
||||||
|
}
|
||||||
|
}, REFRESH_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止定时刷新
|
||||||
|
function stopRefreshTimer() {
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearInterval(refreshTimer)
|
||||||
|
refreshTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面显示时加载数据
|
||||||
|
onShow(() => {
|
||||||
|
// 恢复会话(如果需要)
|
||||||
|
if (!userStore.isLoggedIn) {
|
||||||
|
userStore.restoreSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
fetchTodaySchedules()
|
||||||
|
if (userStore.isLoggedIn) {
|
||||||
|
fetchOrderStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始定时刷新
|
||||||
|
startRefreshTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面离开时停止定时器
|
||||||
|
onHide(() => {
|
||||||
|
stopRefreshTimer()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #F5F5F5;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 欢迎区域 */
|
||||||
|
.welcome-section {
|
||||||
|
padding: 30rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title {
|
||||||
|
font-size: 44rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-date {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-count {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #007AFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日程列表 */
|
||||||
|
.schedule-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
background-color: #F8F9FA;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-date {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 22rpx;
|
||||||
|
text-align: center;
|
||||||
|
white-space: pre-line;
|
||||||
|
line-height: 1.4;
|
||||||
|
min-width: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-user {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单统计 */
|
||||||
|
.order-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num.stat-warning {
|
||||||
|
color: #FF9500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 快捷入口 */
|
||||||
|
.quick-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx 50rpx;
|
||||||
|
background-color: #F8F9FA;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-icon {
|
||||||
|
font-size: 60rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- Logo 区域 -->
|
||||||
|
<view class="logo-section">
|
||||||
|
<image class="logo-img" src="/static/logo.png" mode="aspectFit" />
|
||||||
|
<text class="app-name">OPS 管理系统</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录表单 -->
|
||||||
|
<view class="form">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
v-model="username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
:disabled="loading"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="password"
|
||||||
|
v-model="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
:disabled="loading"
|
||||||
|
@confirm="handleLogin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 记住登录 -->
|
||||||
|
<view class="remember-row">
|
||||||
|
<checkbox-group @change="onRememberChange">
|
||||||
|
<label class="remember-label">
|
||||||
|
<checkbox :checked="remember" value="1" color="#007AFF" />
|
||||||
|
<text>记住登录</text>
|
||||||
|
</label>
|
||||||
|
</checkbox-group>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="submit-btn"
|
||||||
|
@click="handleLogin"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
{{ loading ? '登录中...' : '登录' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 错误提示 -->
|
||||||
|
<view v-if="errorMsg" class="error-tip">
|
||||||
|
{{ errorMsg }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<my-toast ref="toast" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useUserStore } from '../../stores/user'
|
||||||
|
import { useConfigStore } from '../../stores/config'
|
||||||
|
import { userApi } from '../../api/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const remember = ref(true)
|
||||||
|
const loading = ref(false)
|
||||||
|
const errorMsg = ref('')
|
||||||
|
const toast = ref(null)
|
||||||
|
|
||||||
|
// 页面加载时检查是否已配置 API 地址
|
||||||
|
onMounted(() => {
|
||||||
|
const baseUrl = configStore.getApiBaseUrl()
|
||||||
|
if (!baseUrl) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '请先在设置页面配置 API 地址',
|
||||||
|
showCancel: false,
|
||||||
|
success: () => {
|
||||||
|
uni.navigateTo({ url: '/pages/settings/settings' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 记住登录切换
|
||||||
|
const onRememberChange = (e) => {
|
||||||
|
remember.value = e.detail.value.includes('1')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理登录
|
||||||
|
const handleLogin = async () => {
|
||||||
|
// 验证输入
|
||||||
|
if (!username.value.trim()) {
|
||||||
|
errorMsg.value = '请输入用户名'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!password.value) {
|
||||||
|
errorMsg.value = '请输入密码'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 API 地址
|
||||||
|
const baseUrl = configStore.getApiBaseUrl()
|
||||||
|
if (!baseUrl) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '请先在设置页面配置 API 地址',
|
||||||
|
showCancel: false,
|
||||||
|
success: () => {
|
||||||
|
uni.navigateTo({ url: '/pages/settings/settings' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMsg.value = ''
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await userApi.login(username.value, password.value, remember.value)
|
||||||
|
|
||||||
|
if (res.errCode === 0 && res.data?.cookie) {
|
||||||
|
// 登录成功
|
||||||
|
toast.value.success('登录成功')
|
||||||
|
userStore.login(res.data.cookie)
|
||||||
|
|
||||||
|
// 延迟跳转,等待 Toast 显示
|
||||||
|
setTimeout(() => {
|
||||||
|
// 跳转到用户中心(TabBar 页面)
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/user/user'
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
// 登录失败,显示统一错误信息(不区分具体错误,防止暴力破解)
|
||||||
|
errorMsg.value = '用户名或密码错误'
|
||||||
|
toast.value.error(errorMsg.value)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('登录异常', e)
|
||||||
|
errorMsg.value = '网络错误,请检查网络连接'
|
||||||
|
toast.value.error('网络错误')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 100rpx 40rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo 区域 */
|
||||||
|
.logo-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
letter-spacing: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单区域 */
|
||||||
|
.form {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
height: 90rpx;
|
||||||
|
line-height: 90rpx;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
background-color: #F8F8F8;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:last-of-type {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 记住登录 */
|
||||||
|
.remember-row {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-label checkbox {
|
||||||
|
margin-right: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录按钮 */
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 90rpx;
|
||||||
|
line-height: 90rpx;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 32rpx;
|
||||||
|
border-radius: 45rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn[disabled] {
|
||||||
|
background: #CCCCCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误提示 */
|
||||||
|
.error-tip {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 30rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 10rpx;
|
||||||
|
text-align: center;
|
||||||
|
color: #FF3B30;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<text class="placeholder">消息</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,762 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<view class="page-header">
|
||||||
|
<text class="page-title">编辑订单</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="form-content" v-if="!loading">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-title">基本信息</view>
|
||||||
|
|
||||||
|
<!-- 标题(必填) -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label required">配件名称</text>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
v-model="form.title"
|
||||||
|
placeholder="请输入配件名称"
|
||||||
|
maxlength="50"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 备注 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">备注</text>
|
||||||
|
<textarea
|
||||||
|
class="form-textarea"
|
||||||
|
v-model="form.remark"
|
||||||
|
placeholder="请输入备注信息"
|
||||||
|
maxlength="256"
|
||||||
|
/>
|
||||||
|
<text class="char-count">{{ form.remark.length }}/256</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 采购链接 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">采购链接</text>
|
||||||
|
<textarea
|
||||||
|
class="form-textarea"
|
||||||
|
v-model="form.link"
|
||||||
|
placeholder="请输入采购链接"
|
||||||
|
rows="2"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 款式标签 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">款式标签</text>
|
||||||
|
<view class="tags-input">
|
||||||
|
<view class="tag-list">
|
||||||
|
<view
|
||||||
|
class="tag"
|
||||||
|
v-for="(tag, index) in tags"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
<text class="tag-remove" @click="removeTag(index)">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<input
|
||||||
|
class="tag-input"
|
||||||
|
v-model="newTag"
|
||||||
|
placeholder="输入标签后回车添加"
|
||||||
|
@confirm="addTag"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 费用明细 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-title">费用明细</view>
|
||||||
|
|
||||||
|
<!-- 已添加的费用列表 -->
|
||||||
|
<view class="cost-table" v-if="costEntries.length > 0">
|
||||||
|
<view class="cost-header">
|
||||||
|
<text class="cost-col">类型</text>
|
||||||
|
<text class="cost-col">数量</text>
|
||||||
|
<text class="cost-col">单价</text>
|
||||||
|
<text class="cost-col">总计</text>
|
||||||
|
<text class="cost-col">货币</text>
|
||||||
|
<text class="cost-col">操作</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="cost-row"
|
||||||
|
v-for="(item, index) in costEntries"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<text class="cost-col">{{ costType[item.type] || item.type }}</text>
|
||||||
|
<text class="cost-col">{{ item.int }}</text>
|
||||||
|
<text class="cost-col">{{ item.cost }}</text>
|
||||||
|
<text class="cost-col">{{ item.costt }}</text>
|
||||||
|
<text class="cost-col">{{ currencyOptions[item.currencytype] || item.currencytype }}</text>
|
||||||
|
<text class="cost-col delete" @click="removeCostEntry(index)">删除</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 添加费用表单 -->
|
||||||
|
<view class="cost-form">
|
||||||
|
<view class="cost-form-row">
|
||||||
|
<view class="cost-field">
|
||||||
|
<text class="cost-label">费用类型</text>
|
||||||
|
<picker mode="selector" :range="costTypeOptions" range-key="label" @change="onCostTypeChange">
|
||||||
|
<view class="cost-picker">
|
||||||
|
{{ costTypeOptions[costTypeIndex].label }}
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<view class="cost-field">
|
||||||
|
<text class="cost-label">数量</text>
|
||||||
|
<input class="cost-input" type="number" v-model="newCost.int" min="1" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="cost-form-row">
|
||||||
|
<view class="cost-field">
|
||||||
|
<text class="cost-label">单价</text>
|
||||||
|
<input class="cost-input" type="digit" v-model="newCost.cost" step="0.01" />
|
||||||
|
</view>
|
||||||
|
<view class="cost-field">
|
||||||
|
<text class="cost-label">货币</text>
|
||||||
|
<picker mode="selector" :range="currencyOptionsList" range-key="label" @change="onCurrencyChange">
|
||||||
|
<view class="cost-picker">
|
||||||
|
{{ currencyOptionsList[currencyIndex].label }}
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="cost-total" v-if="newCostCost > 0">
|
||||||
|
总计:{{ currencyOptionsList[currencyIndex].symbol }}{{ newCostTotal }}
|
||||||
|
</view>
|
||||||
|
<view class="cost-error" v-if="costError">单价必须大于0</view>
|
||||||
|
<button class="add-cost-btn" @click="addCostEntry">添加费用</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 图片上传 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-title">图片</view>
|
||||||
|
<view class="photo-upload">
|
||||||
|
<view
|
||||||
|
v-for="(photo, index) in photos"
|
||||||
|
:key="index"
|
||||||
|
class="photo-item"
|
||||||
|
@click="previewImage(index)"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="photo-img"
|
||||||
|
:src="photo.url"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="photo-remove" @click.stop="removePhoto(index)">×</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="photos.length < 9" class="photo-add" @click="chooseImage">
|
||||||
|
<text class="photo-add-icon">+</text>
|
||||||
|
<text class="photo-add-text">添加图片</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<view class="submit-area">
|
||||||
|
<button class="submit-btn" :disabled="submitting" @click="handleSubmit">
|
||||||
|
{{ submitting ? '提交中...' : '保存修改' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view v-else class="loading-view"><text>加载中...</text></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { purchaseApi } from '@/api/purchase.js'
|
||||||
|
import { api } from '@/api/index.js'
|
||||||
|
import { useConfigStore } from '@/stores/config.js'
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
// 订单ID
|
||||||
|
const orderId = ref(null)
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
// 费用类型选项
|
||||||
|
const costTypeOptions = [
|
||||||
|
{ value: 1, label: '单价' },
|
||||||
|
{ value: 2, label: '运费' }
|
||||||
|
]
|
||||||
|
const costTypeIndex = ref(0)
|
||||||
|
const costType = { 1: '单价', 2: '运费' }
|
||||||
|
|
||||||
|
// 货币选项
|
||||||
|
const currencyOptionsList = [
|
||||||
|
{ value: 1, label: 'CNY', symbol: '¥' },
|
||||||
|
{ value: 2, label: 'MOP', symbol: 'MOP' },
|
||||||
|
{ value: 3, label: 'HKD', symbol: 'HK$' },
|
||||||
|
{ value: 4, label: 'USD', symbol: '$' }
|
||||||
|
]
|
||||||
|
const currencyOptions = { 1: 'CNY', 2: 'MOP', 3: 'HKD', 4: 'USD' }
|
||||||
|
const currencyIndex = ref(0)
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const form = reactive({
|
||||||
|
title: '',
|
||||||
|
remark: '',
|
||||||
|
link: '',
|
||||||
|
styles: '',
|
||||||
|
photos: []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 标签
|
||||||
|
const tags = ref([])
|
||||||
|
const newTag = ref('')
|
||||||
|
|
||||||
|
// 费用明细
|
||||||
|
const costEntries = reactive([])
|
||||||
|
const newCost = reactive({
|
||||||
|
type: 1,
|
||||||
|
int: 1,
|
||||||
|
cost: 0,
|
||||||
|
currencyType: 1
|
||||||
|
})
|
||||||
|
const costError = ref(false)
|
||||||
|
|
||||||
|
// 图片
|
||||||
|
const photos = ref([])
|
||||||
|
const newCostCost = computed(() => parseFloat(newCost.cost) || 0)
|
||||||
|
const newCostTotal = computed(() => (newCost.int * newCostCost.value).toFixed(2))
|
||||||
|
|
||||||
|
// 提交状态
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
// 初始化页面
|
||||||
|
onLoad((options) => {
|
||||||
|
if (options?.id) {
|
||||||
|
orderId.value = parseInt(options.id)
|
||||||
|
fetchOrderDetail()
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '缺少订单ID', icon: 'none' })
|
||||||
|
setTimeout(() => uni.navigateBack(), 1500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取订单详情
|
||||||
|
async function fetchOrderDetail() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await purchaseApi.getOrder({ id: orderId.value })
|
||||||
|
if (res.errCode === 0 && res.data?.order) {
|
||||||
|
const order = res.data.order
|
||||||
|
|
||||||
|
// 填充表单 (注意:Go 结构体字段首字母大写)
|
||||||
|
form.title = order.Title || ''
|
||||||
|
form.remark = order.Remark || ''
|
||||||
|
form.link = order.Link || ''
|
||||||
|
form.styles = order.Styles || ''
|
||||||
|
|
||||||
|
// 解析标签
|
||||||
|
if (order.Styles) {
|
||||||
|
tags.value = order.Styles.split(',').filter(t => t.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析费用 (API返回的是数据库模型 Price/Quantity/CurrencyType/CostType)
|
||||||
|
costEntries.length = 0
|
||||||
|
if (res.data.costs && res.data.costs.length > 0) {
|
||||||
|
res.data.costs.forEach(c => {
|
||||||
|
costEntries.push({
|
||||||
|
type: c.CostType,
|
||||||
|
int: c.Quantity,
|
||||||
|
cost: (c.Price / 100).toFixed(2),
|
||||||
|
costt: ((c.Price * c.Quantity) / 100).toFixed(2),
|
||||||
|
currencytype: c.CurrencyType
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析图片
|
||||||
|
photos.value = []
|
||||||
|
if (res.data.photos && res.data.photos.length > 0) {
|
||||||
|
res.data.photos.forEach(p => {
|
||||||
|
photos.value.push({
|
||||||
|
hash: p.Sha256,
|
||||||
|
url: `${configStore.getFileBaseUrl()}/api/files/get/${p.Sha256}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '获取订单信息失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取订单详情失败', e)
|
||||||
|
uni.showToast({ title: '获取订单信息失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加标签
|
||||||
|
function addTag() {
|
||||||
|
if (newTag.value.trim()) {
|
||||||
|
tags.value.push(newTag.value.trim())
|
||||||
|
newTag.value = ''
|
||||||
|
form.styles = tags.value.join(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除标签
|
||||||
|
function removeTag(index) {
|
||||||
|
tags.value.splice(index, 1)
|
||||||
|
form.styles = tags.value.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 费用类型选择
|
||||||
|
function onCostTypeChange(e) {
|
||||||
|
costTypeIndex.value = e.detail.value
|
||||||
|
newCost.type = costTypeOptions[e.detail.value].value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 货币类型选择
|
||||||
|
function onCurrencyChange(e) {
|
||||||
|
currencyIndex.value = e.detail.value
|
||||||
|
newCost.currencyType = currencyOptionsList[e.detail.value].value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加费用
|
||||||
|
function addCostEntry() {
|
||||||
|
if (newCost.cost <= 0) {
|
||||||
|
costError.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
costError.value = false
|
||||||
|
costEntries.push({
|
||||||
|
type: newCost.type,
|
||||||
|
int: newCost.int,
|
||||||
|
cost: parseFloat(newCost.cost).toFixed(2),
|
||||||
|
costt: newCostTotal.value,
|
||||||
|
currencytype: newCost.currencyType
|
||||||
|
})
|
||||||
|
// 重置
|
||||||
|
newCost.type = 1
|
||||||
|
newCost.int = 1
|
||||||
|
newCost.cost = 0
|
||||||
|
newCost.currencyType = 1
|
||||||
|
costTypeIndex.value = 0
|
||||||
|
currencyIndex.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除费用
|
||||||
|
function removeCostEntry(index) {
|
||||||
|
costEntries.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择图片
|
||||||
|
function chooseImage() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 9 - photos.value.length,
|
||||||
|
success: (res) => {
|
||||||
|
res.tempFiles.forEach(file => {
|
||||||
|
uploadImage(file.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传图片
|
||||||
|
async function uploadImage(filePath) {
|
||||||
|
uni.showLoading({ title: '上传中...', mask: true })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.upload('/files/upload/image', { uri: filePath, name: 'file' })
|
||||||
|
if (res.errCode === 0 && res.data?.hash) {
|
||||||
|
photos.value.push({
|
||||||
|
hash: res.data.hash,
|
||||||
|
url: `${configStore.getFileBaseUrl()}/api/files/get/${res.data.hash}`
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('上传失败', e)
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览图片
|
||||||
|
function previewImage(index) {
|
||||||
|
const urls = photos.value.map(p => p.url)
|
||||||
|
uni.previewImage({
|
||||||
|
urls,
|
||||||
|
current: index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除图片
|
||||||
|
function removePhoto(index) {
|
||||||
|
photos.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
async function handleSubmit() {
|
||||||
|
// 验证必填项
|
||||||
|
if (!form.title.trim()) {
|
||||||
|
uni.showToast({ title: '请输入配件名称', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 转换费用数据(元转分)
|
||||||
|
const costs = costEntries.map(h => ({
|
||||||
|
type: h.type,
|
||||||
|
int: h.int,
|
||||||
|
cost: Math.round(parseFloat(h.cost) * 100),
|
||||||
|
costt: Math.round(parseFloat(h.costt) * 100),
|
||||||
|
currencytype: h.currencytype
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 转换图片
|
||||||
|
const photoHashes = photos.value.map(p => p.hash)
|
||||||
|
|
||||||
|
const res = await purchaseApi.updateOrder({
|
||||||
|
id: orderId.value,
|
||||||
|
title: form.title,
|
||||||
|
remark: form.remark,
|
||||||
|
link: form.link,
|
||||||
|
styles: form.styles,
|
||||||
|
costs,
|
||||||
|
photos: photoHashes
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.errCode === 0) {
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||||
|
uni.$emit('purchase-refresh')
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('提交失败', e)
|
||||||
|
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label.required::after {
|
||||||
|
content: ' *';
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-count {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签输入 */
|
||||||
|
.tags-input {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #e6f0ff;
|
||||||
|
color: #1890ff;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove {
|
||||||
|
margin-left: 8rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 60rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 费用明细 */
|
||||||
|
.cost-table {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-header,
|
||||||
|
.cost-row {
|
||||||
|
display: flex;
|
||||||
|
padding: 16rpx 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-header {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-row {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-col {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-col.delete {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-form {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 72rpx;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-picker {
|
||||||
|
height: 72rpx;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
line-height: 72rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-total {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-error {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #f56c6c;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-cost-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
background-color: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片上传 */
|
||||||
|
.photo-upload {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-item {
|
||||||
|
position: relative;
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-remove {
|
||||||
|
position: absolute;
|
||||||
|
top: -12rpx;
|
||||||
|
right: -12rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: #f56c6c;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 28rpx;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-add {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 2rpx dashed #ddd;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-add-icon {
|
||||||
|
font-size: 60rpx;
|
||||||
|
color: #ccc;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-add-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提交按钮 */
|
||||||
|
.submit-area {
|
||||||
|
padding: 20rpx 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
background-color: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn[disabled] {
|
||||||
|
background-color: #a0cfff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-view {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 300rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,679 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<view class="page-header">
|
||||||
|
<text class="page-title">新增订单</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="form-content">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-title">基本信息</view>
|
||||||
|
|
||||||
|
<!-- 标题(必填) -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label required">配件名称</text>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
v-model="form.title"
|
||||||
|
placeholder="请输入配件名称"
|
||||||
|
maxlength="50"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 备注 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">备注</text>
|
||||||
|
<textarea
|
||||||
|
class="form-textarea"
|
||||||
|
v-model="form.remark"
|
||||||
|
placeholder="请输入备注信息"
|
||||||
|
maxlength="256"
|
||||||
|
/>
|
||||||
|
<text class="char-count">{{ form.remark.length }}/256</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 采购链接 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">采购链接</text>
|
||||||
|
<textarea
|
||||||
|
class="form-textarea"
|
||||||
|
v-model="form.link"
|
||||||
|
placeholder="请输入采购链接"
|
||||||
|
rows="2"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 款式标签 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">款式标签</text>
|
||||||
|
<view class="tags-input">
|
||||||
|
<view class="tag-list">
|
||||||
|
<view
|
||||||
|
class="tag"
|
||||||
|
v-for="(tag, index) in tags"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
<text class="tag-remove" @click="removeTag(index)">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<input
|
||||||
|
class="tag-input"
|
||||||
|
v-model="newTag"
|
||||||
|
placeholder="输入标签后回车添加"
|
||||||
|
@confirm="addTag"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 费用明细 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-title">费用明细</view>
|
||||||
|
|
||||||
|
<!-- 已添加的费用列表 -->
|
||||||
|
<view class="cost-table" v-if="costEntries.length > 0">
|
||||||
|
<view class="cost-header">
|
||||||
|
<text class="cost-col">类型</text>
|
||||||
|
<text class="cost-col">数量</text>
|
||||||
|
<text class="cost-col">单价</text>
|
||||||
|
<text class="cost-col">总计</text>
|
||||||
|
<text class="cost-col">货币</text>
|
||||||
|
<text class="cost-col">操作</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="cost-row"
|
||||||
|
v-for="(item, index) in costEntries"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<text class="cost-col">{{ costType[item.type] || item.type }}</text>
|
||||||
|
<text class="cost-col">{{ item.int }}</text>
|
||||||
|
<text class="cost-col">{{ item.cost }}</text>
|
||||||
|
<text class="cost-col">{{ item.costt }}</text>
|
||||||
|
<text class="cost-col">{{ currencyOptions[item.currencytype] || item.currencytype }}</text>
|
||||||
|
<text class="cost-col delete" @click="removeCostEntry(index)">删除</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 添加费用表单 -->
|
||||||
|
<view class="cost-form">
|
||||||
|
<view class="cost-form-row">
|
||||||
|
<view class="cost-field">
|
||||||
|
<text class="cost-label">费用类型</text>
|
||||||
|
<picker mode="selector" :range="costTypeOptions" range-key="label" @change="onCostTypeChange">
|
||||||
|
<view class="cost-picker">
|
||||||
|
{{ costTypeOptions[costTypeIndex].label }}
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<view class="cost-field">
|
||||||
|
<text class="cost-label">数量</text>
|
||||||
|
<input class="cost-input" type="number" v-model="newCost.int" min="1" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="cost-form-row">
|
||||||
|
<view class="cost-field">
|
||||||
|
<text class="cost-label">单价</text>
|
||||||
|
<input class="cost-input" type="digit" v-model="newCost.cost" step="0.01" />
|
||||||
|
</view>
|
||||||
|
<view class="cost-field">
|
||||||
|
<text class="cost-label">货币</text>
|
||||||
|
<picker mode="selector" :range="currencyOptionsList" range-key="label" @change="onCurrencyChange">
|
||||||
|
<view class="cost-picker">
|
||||||
|
{{ currencyOptionsList[currencyIndex].label }}
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="cost-total" v-if="newCostCost > 0">
|
||||||
|
总计:{{ currencyOptionsList[currencyIndex].symbol }}{{ newCostTotal }}
|
||||||
|
</view>
|
||||||
|
<view class="cost-error" v-if="costError">单价必须大于0</view>
|
||||||
|
<button class="add-cost-btn" @click="addCostEntry">添加费用</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 图片上传 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-title">图片</view>
|
||||||
|
<view class="photo-upload">
|
||||||
|
<view
|
||||||
|
v-for="(photo, index) in photos"
|
||||||
|
:key="index"
|
||||||
|
class="photo-item"
|
||||||
|
@click="previewImage(index)"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="photo-img"
|
||||||
|
:src="photo.url"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="photo-remove" @click.stop="removePhoto(index)">×</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="photos.length < 9" class="photo-add" @click="chooseImage">
|
||||||
|
<text class="photo-add-icon">+</text>
|
||||||
|
<text class="photo-add-text">添加图片</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<view class="submit-area">
|
||||||
|
<button class="submit-btn" :disabled="submitting" @click="handleSubmit">
|
||||||
|
{{ submitting ? '提交中...' : '提交订单' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { purchaseApi } from '@/api/purchase.js'
|
||||||
|
import { api } from '@/api/index.js'
|
||||||
|
import { useConfigStore } from '@/stores/config.js'
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
// 费用类型选项
|
||||||
|
const costTypeOptions = [
|
||||||
|
{ value: 1, label: '单价' },
|
||||||
|
{ value: 2, label: '运费' }
|
||||||
|
]
|
||||||
|
const costTypeIndex = ref(0)
|
||||||
|
const costType = { 1: '单价', 2: '运费' }
|
||||||
|
|
||||||
|
// 货币选项
|
||||||
|
const currencyOptionsList = [
|
||||||
|
{ value: 1, label: 'CNY', symbol: '¥' },
|
||||||
|
{ value: 2, label: 'MOP', symbol: 'MOP' },
|
||||||
|
{ value: 3, label: 'HKD', symbol: 'HK$' },
|
||||||
|
{ value: 4, label: 'USD', symbol: '$' }
|
||||||
|
]
|
||||||
|
const currencyOptions = { 1: 'CNY', 2: 'MOP', 3: 'HKD', 4: 'USD' }
|
||||||
|
const currencyIndex = ref(0)
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const form = reactive({
|
||||||
|
title: '',
|
||||||
|
remark: '',
|
||||||
|
link: '',
|
||||||
|
styles: '',
|
||||||
|
photos: []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 标签
|
||||||
|
const tags = ref([])
|
||||||
|
const newTag = ref('')
|
||||||
|
|
||||||
|
// 费用明细
|
||||||
|
const costEntries = reactive([])
|
||||||
|
const newCost = reactive({
|
||||||
|
type: 1,
|
||||||
|
int: 1,
|
||||||
|
cost: 0,
|
||||||
|
currencyType: 1
|
||||||
|
})
|
||||||
|
const costError = ref(false)
|
||||||
|
|
||||||
|
// 图片
|
||||||
|
const photos = ref([])
|
||||||
|
const newCostCost = computed(() => parseFloat(newCost.cost) || 0)
|
||||||
|
const newCostTotal = computed(() => (newCost.int * newCostCost.value).toFixed(2))
|
||||||
|
|
||||||
|
// 提交状态
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
// 添加标签
|
||||||
|
function addTag() {
|
||||||
|
if (newTag.value.trim()) {
|
||||||
|
tags.value.push(newTag.value.trim())
|
||||||
|
newTag.value = ''
|
||||||
|
form.styles = tags.value.join(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除标签
|
||||||
|
function removeTag(index) {
|
||||||
|
tags.value.splice(index, 1)
|
||||||
|
form.styles = tags.value.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 费用类型选择
|
||||||
|
function onCostTypeChange(e) {
|
||||||
|
costTypeIndex.value = e.detail.value
|
||||||
|
newCost.type = costTypeOptions[e.detail.value].value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 货币类型选择
|
||||||
|
function onCurrencyChange(e) {
|
||||||
|
currencyIndex.value = e.detail.value
|
||||||
|
newCost.currencyType = currencyOptionsList[e.detail.value].value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加费用
|
||||||
|
function addCostEntry() {
|
||||||
|
if (newCost.cost <= 0) {
|
||||||
|
costError.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
costError.value = false
|
||||||
|
costEntries.push({
|
||||||
|
type: newCost.type,
|
||||||
|
int: newCost.int,
|
||||||
|
cost: parseFloat(newCost.cost).toFixed(2),
|
||||||
|
costt: newCostTotal.value,
|
||||||
|
currencytype: newCost.currencyType
|
||||||
|
})
|
||||||
|
// 重置
|
||||||
|
newCost.type = 1
|
||||||
|
newCost.int = 1
|
||||||
|
newCost.cost = 0
|
||||||
|
newCost.currencyType = 1
|
||||||
|
costTypeIndex.value = 0
|
||||||
|
currencyIndex.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除费用
|
||||||
|
function removeCostEntry(index) {
|
||||||
|
costEntries.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择图片
|
||||||
|
function chooseImage() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 9,
|
||||||
|
success: (res) => {
|
||||||
|
res.tempFiles.forEach(file => {
|
||||||
|
uploadImage(file.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传图片
|
||||||
|
async function uploadImage(filePath) {
|
||||||
|
uni.showLoading({ title: '上传中...', mask: true })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.upload('/files/upload/image', { uri: filePath, name: 'file' })
|
||||||
|
if (res.errCode === 0 && res.data?.hash) {
|
||||||
|
photos.value.push({
|
||||||
|
hash: res.data.hash,
|
||||||
|
url: `${configStore.getFileBaseUrl()}/api/files/get/${res.data.hash}`
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('上传失败', e)
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览图片
|
||||||
|
function previewImage(index) {
|
||||||
|
const urls = photos.value.map(p => p.url)
|
||||||
|
uni.previewImage({
|
||||||
|
urls,
|
||||||
|
current: index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除图片
|
||||||
|
function removePhoto(index) {
|
||||||
|
photos.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
async function handleSubmit() {
|
||||||
|
// 验证必填项
|
||||||
|
if (!form.title.trim()) {
|
||||||
|
uni.showToast({ title: '请输入配件名称', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 转换费用数据(元转分)
|
||||||
|
const costs = costEntries.map(h => ({
|
||||||
|
type: h.type,
|
||||||
|
int: h.int,
|
||||||
|
cost: Math.round(parseFloat(h.cost) * 100),
|
||||||
|
costt: Math.round(parseFloat(h.costt) * 100),
|
||||||
|
currencytype: h.currencytype
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 转换图片
|
||||||
|
const photoHashes = photos.value.map(p => p.hash)
|
||||||
|
|
||||||
|
const res = await purchaseApi.addOrder({
|
||||||
|
title: form.title,
|
||||||
|
remark: form.remark,
|
||||||
|
link: form.link,
|
||||||
|
styles: form.styles,
|
||||||
|
costs,
|
||||||
|
photos: photoHashes
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.errCode === 0) {
|
||||||
|
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||||
|
uni.$emit('purchase-refresh')
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '提交失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('提交失败', e)
|
||||||
|
uni.showToast({ title: '提交失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label.required::after {
|
||||||
|
content: ' *';
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-count {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签输入 */
|
||||||
|
.tags-input {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #e6f0ff;
|
||||||
|
color: #1890ff;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove {
|
||||||
|
margin-left: 8rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 60rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 费用明细 */
|
||||||
|
.cost-table {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-header,
|
||||||
|
.cost-row {
|
||||||
|
display: flex;
|
||||||
|
padding: 16rpx 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-header {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-row {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-col {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-col.delete {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-form {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 72rpx;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-picker {
|
||||||
|
height: 72rpx;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
line-height: 72rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-total {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-error {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #f56c6c;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-cost-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
background-color: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片上传 */
|
||||||
|
.photo-upload {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-item {
|
||||||
|
position: relative;
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-remove {
|
||||||
|
position: absolute;
|
||||||
|
top: -12rpx;
|
||||||
|
right: -12rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: #f56c6c;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 28rpx;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-add {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 2rpx dashed #ddd;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-add-icon {
|
||||||
|
font-size: 60rpx;
|
||||||
|
color: #ccc;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-add-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提交按钮 */
|
||||||
|
.submit-area {
|
||||||
|
padding: 20rpx 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
background-color: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn[disabled] {
|
||||||
|
background-color: #a0cfff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="header">
|
||||||
|
<text class="back-btn" @click="goBack">‹ 返回</text>
|
||||||
|
<text class="title">订单详情</text>
|
||||||
|
<view class="header-actions">
|
||||||
|
<text v-if="order" class="print-btn" @click="printOrder">🖨</text>
|
||||||
|
<text v-if="canModify" class="edit-btn" @click="goEdit">编辑</text>
|
||||||
|
<view v-else class="header-right"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="content" refresher-enabled @refresherrefresh="onRefresh" :refresher-triggered="refreshing">
|
||||||
|
<view v-if="loading" class="loading"><text>加载中...</text></view>
|
||||||
|
<view v-else-if="order">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="card-title">订单信息</text>
|
||||||
|
<text class="order-status" :class="order.OrderStatus">{{ getStatusText(order.OrderStatus) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">标题</text>
|
||||||
|
<text class="info-value">{{ order.Title || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="order.Link">
|
||||||
|
<text class="info-label">链接</text>
|
||||||
|
<text class="info-value link" @click="openLink">{{ order.Link }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="order.Styles">
|
||||||
|
<text class="info-label">样式</text>
|
||||||
|
<text class="info-value">{{ order.Styles }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="order.Remark">
|
||||||
|
<text class="info-label">备注</text>
|
||||||
|
<text class="info-value">{{ order.Remark }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">创建时间</text>
|
||||||
|
<text class="info-value">{{ formatDate(order.CreatedAt) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 状态切换 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-header"><text class="card-title">变更状态</text></view>
|
||||||
|
<view class="status-buttons">
|
||||||
|
<view v-for="opt in statusOptions" :key="opt.value" class="status-btn" :class="order.OrderStatus === opt.value ? 'active ' + opt.value : ''" @click="openStatusDialog(opt.value)">
|
||||||
|
<text>{{ opt.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 费用明细 -->
|
||||||
|
<view class="card" v-if="costs.length > 0">
|
||||||
|
<view class="card-header"><text class="card-title">费用明细</text></view>
|
||||||
|
<view class="cost-table">
|
||||||
|
<view class="cost-header">
|
||||||
|
<text class="cost-th">类型</text>
|
||||||
|
<text class="cost-th">数量</text>
|
||||||
|
<text class="cost-th">单价</text>
|
||||||
|
<text class="cost-th">小计</text>
|
||||||
|
</view>
|
||||||
|
<view class="cost-row" v-for="(cost, idx) in costs" :key="idx">
|
||||||
|
<text class="cost-td">{{ getCostTypeText(cost.CostType) }}</text>
|
||||||
|
<text class="cost-td">{{ cost.Quantity }}</text>
|
||||||
|
<text class="cost-td">{{ formatPrice(cost.Price) }}</text>
|
||||||
|
<text class="cost-td">{{ formatPrice(cost.Price * cost.Quantity) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="cost-total">
|
||||||
|
<text v-for="g in costsByCurrency" :key="g.currency" class="total-tag">{{ g.currency }} {{ g.total }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 图片 -->
|
||||||
|
<view class="card" v-if="photos.length > 0">
|
||||||
|
<view class="card-header"><text class="card-title">图片 ({{ photos.length }})</text></view>
|
||||||
|
<view class="photo-grid">
|
||||||
|
<view v-for="photo in photos" :key="photo.ID" class="photo-item">
|
||||||
|
<image :src="getImageUrl(photo.Sha256)" mode="aspectFill" @click="previewImages(photo.Sha256)" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关联工单 -->
|
||||||
|
<view class="card" v-if="workOrders.length > 0">
|
||||||
|
<view class="card-header"><text class="card-title">关联工单</text></view>
|
||||||
|
<view v-for="wo in workOrders" :key="wo.id" class="workorder-item" @click="goToWorkOrder(wo.id)">
|
||||||
|
<text class="wo-id">#{{ wo.id }}</text>
|
||||||
|
<text class="wo-title">{{ wo.title }}</text>
|
||||||
|
<text class="wo-status" :class="wo.status">{{ getWOStatusText(wo.status) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 状态记录 -->
|
||||||
|
<view class="card" v-if="commits.length > 0">
|
||||||
|
<view class="card-header"><text class="card-title">状态记录</text></view>
|
||||||
|
<view class="timeline">
|
||||||
|
<view v-for="commit in commits" :key="commit.id" class="timeline-item">
|
||||||
|
<view class="timeline-dot" :class="commit.status"></view>
|
||||||
|
<view class="timeline-content">
|
||||||
|
<view class="timeline-header">
|
||||||
|
<text class="timeline-status" :class="commit.status">{{ getStatusText(commit.status) }}</text>
|
||||||
|
<text class="timeline-date">{{ formatDate(commit.createdAt) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="timeline-user">by {{ getUsernameById(commit.userId) }}</text>
|
||||||
|
<text class="timeline-comment" v-if="commit.comment">{{ commit.comment }}</text>
|
||||||
|
<view class="timeline-photos" v-if="commit.photos && commit.photos.length">
|
||||||
|
<image v-for="(hash, idx) in commit.photos" :key="hash" class="timeline-photo" :src="getImageUrl(hash)" mode="aspectFill" @click="previewCommitImages(commit.photos, idx)" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty"><text>未找到订单信息</text></view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 状态变更弹窗 -->
|
||||||
|
<view class="modal" v-if="statusDialogVisible" @click="closeStatusDialog">
|
||||||
|
<view class="modal-content" @click.stop>
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">变更状态</text>
|
||||||
|
<text class="modal-close" @click="closeStatusDialog">×</text>
|
||||||
|
</view>
|
||||||
|
<view class="modal-body">
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">新状态</text>
|
||||||
|
<view class="status-display">{{ getStatusText(pendingStatus) }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">备注</text>
|
||||||
|
<textarea v-model="pendingComment" class="form-textarea" placeholder="可选" />
|
||||||
|
</view>
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">图片</text>
|
||||||
|
<view class="photo-upload-area">
|
||||||
|
<view v-for="(photo, idx) in pendingPhotos" :key="idx" class="uploaded-photo">
|
||||||
|
<image :src="photo.url" mode="aspectFill" />
|
||||||
|
<view class="photo-remove" @click="pendingPhotos.splice(idx, 1)">×</view>
|
||||||
|
</view>
|
||||||
|
<view class="add-photo-btn" @click="chooseImage" v-if="pendingPhotos.length < 9">
|
||||||
|
<text>+</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="modal-footer">
|
||||||
|
<text class="btn-cancel" @click="closeStatusDialog">取消</text>
|
||||||
|
<text class="btn-confirm" @click="confirmStatusChange">确认</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { purchaseApi } from '@/api/purchase.js'
|
||||||
|
import { useConfigStore } from '@/stores/config.js'
|
||||||
|
import { fetchUserInfo, getUsername } from '@/stores/users.js'
|
||||||
|
import { api } from '@/api/index.js'
|
||||||
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
const orderId = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const order = ref(null)
|
||||||
|
const costs = ref([])
|
||||||
|
const photos = ref([])
|
||||||
|
const commits = ref([])
|
||||||
|
const workOrders = ref([])
|
||||||
|
const canModify = ref(false)
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: 'pending', label: '待处理' },
|
||||||
|
{ value: 'ordered', label: '已下单' },
|
||||||
|
{ value: 'arrived', label: '已到达' },
|
||||||
|
{ value: 'received', label: '已收件' },
|
||||||
|
{ value: 'lost', label: '丢件' },
|
||||||
|
{ value: 'returned', label: '退件' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const currencyOptions = { 1: 'CNY', 2: 'MOP', 3: 'HKD', 4: 'USD' }
|
||||||
|
const costTypeOptions = { 1: '单价', 2: '运费' }
|
||||||
|
|
||||||
|
const costsByCurrency = computed(() => {
|
||||||
|
const groups = {}
|
||||||
|
costs.value.forEach(c => {
|
||||||
|
const cur = currencyOptions[c.CurrencyType] || 'Unknown'
|
||||||
|
const amount = c.Price && c.Quantity ? ((c.Price * c.Quantity) / 100).toFixed(2) : '0.00'
|
||||||
|
if (!groups[cur]) groups[cur] = 0
|
||||||
|
groups[cur] += parseFloat(amount)
|
||||||
|
})
|
||||||
|
return Object.entries(groups).map(([currency, total]) => ({ currency, total: total.toFixed(2) }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusDialogVisible = ref(false)
|
||||||
|
const pendingStatus = ref('')
|
||||||
|
const pendingComment = ref('')
|
||||||
|
const pendingPhotos = ref([])
|
||||||
|
const submitting = ref(false)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
|
||||||
|
function getStatusText(status) {
|
||||||
|
return statusOptions.find(s => s.value === status)?.label || status
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrencyText(type) { return currencyOptions[type] || '-' }
|
||||||
|
function getCostTypeText(type) { return costTypeOptions[type] || type }
|
||||||
|
function getWOStatusText(status) {
|
||||||
|
return { pending: '待处理', checked: '已检查', parts_ordered: '已下单零件', repaired: '已维修', returned: '已送还', unrepairable: '无法维修' }[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
const d = new Date(dateStr)
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPrice(priceInCents) {
|
||||||
|
return priceInCents ? (priceInCents / 100).toFixed(2) : '0.00'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageUrl(sha256) {
|
||||||
|
return configStore.getFileBaseUrl() + '/api/files/get/' + sha256
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewImages(currentSha) {
|
||||||
|
const allPhotos = photos.value || []
|
||||||
|
const urls = allPhotos.map(p => getImageUrl(p.Sha256))
|
||||||
|
const idx = allPhotos.findIndex(p => p.Sha256 === currentSha)
|
||||||
|
uni.previewImage({ urls, current: idx >= 0 ? idx : 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewCommitImages(photoHashes, currentIndex) {
|
||||||
|
const urls = photoHashes.map(h => getImageUrl(h))
|
||||||
|
uni.previewImage({ urls, current: currentIndex })
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUsernameById(userId) {
|
||||||
|
return getUsername(userId) || `用户${userId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchOrderDetail() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await purchaseApi.getOrder({ id: orderId.value })
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
order.value = res.data.order || null
|
||||||
|
canModify.value = res.data.canModify || false
|
||||||
|
costs.value = res.data.costs || []
|
||||||
|
photos.value = res.data.photos || []
|
||||||
|
commits.value = res.data.commits || []
|
||||||
|
workOrders.value = res.data.workOrders || []
|
||||||
|
|
||||||
|
// 预取用户信息
|
||||||
|
if (order.value?.UserID) fetchUserInfo(order.value.UserID)
|
||||||
|
commits.value.forEach(c => {
|
||||||
|
if (c.userId) fetchUserInfo(c.userId)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
order.value = null
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取订单详情失败', e)
|
||||||
|
order.value = null
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openStatusDialog(status) {
|
||||||
|
pendingStatus.value = status
|
||||||
|
pendingComment.value = ''
|
||||||
|
pendingPhotos.value = []
|
||||||
|
statusDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeStatusDialog() {
|
||||||
|
statusDialogVisible.value = false
|
||||||
|
pendingStatus.value = ''
|
||||||
|
pendingComment.value = ''
|
||||||
|
pendingPhotos.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseImage() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 9 - pendingPhotos.value.length,
|
||||||
|
success: (res) => res.tempFilePaths.forEach(path => uploadImage(path))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImage(filePath) {
|
||||||
|
try {
|
||||||
|
const res = await api.upload('/files/upload/image', { uri: filePath, name: 'file' })
|
||||||
|
if (res.errCode === 0 && res.data?.hash) {
|
||||||
|
pendingPhotos.value.push({ hash: res.data.hash, url: getImageUrl(res.data.hash) })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('上传失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmStatusChange() {
|
||||||
|
if (submitting.value) return
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.post('/purchase/updatestatus', {
|
||||||
|
id: orderId.value,
|
||||||
|
status: pendingStatus.value,
|
||||||
|
comment: pendingComment.value,
|
||||||
|
photos: pendingPhotos.value.map(p => p.hash)
|
||||||
|
})
|
||||||
|
if (res.errCode === 0) {
|
||||||
|
uni.showToast({ title: '状态更新成功', icon: 'success' })
|
||||||
|
closeStatusDialog()
|
||||||
|
fetchOrderDetail()
|
||||||
|
uni.$emit('purchase-refresh')
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.errMsg || '更新失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({ title: '更新失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() { uni.navigateBack() }
|
||||||
|
|
||||||
|
function printOrder() {
|
||||||
|
if (!order.value) return
|
||||||
|
|
||||||
|
// #ifndef APP-PLUS
|
||||||
|
uni.showToast({ title: '打印功能仅在 App 端可用', icon: 'none' })
|
||||||
|
return
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
const printer = uni.requireNativePlugin('LcPrinter')
|
||||||
|
|
||||||
|
// 初始化打印机
|
||||||
|
printer.initPrinter({})
|
||||||
|
printer.setConcentration({ level: 39 })
|
||||||
|
printer.setLineSpacing({ spacing: 1 })
|
||||||
|
|
||||||
|
// 标签打印模式(使用黑标定位)
|
||||||
|
printer.printEnableMark({ enable: true })
|
||||||
|
|
||||||
|
// 第一行:标题(加粗大字)
|
||||||
|
printer.setFontSize({ fontSize: 1 })
|
||||||
|
printer.setTextBold({ bold: true })
|
||||||
|
printer.printText({ content: (order.value.Title || '(无标题)')+'\n' })
|
||||||
|
//printer.printLine({ line_length: 1 })
|
||||||
|
|
||||||
|
// 第二行:备注
|
||||||
|
printer.setFontSize({ fontSize: 0 })
|
||||||
|
printer.setTextBold({ bold: false })
|
||||||
|
printer.printText({ content: '备注: ' + (order.value.Remark || '(无备注)')+'\n' })
|
||||||
|
//printer.printLine({ line_length: 1 })
|
||||||
|
|
||||||
|
// 第三行:样式
|
||||||
|
printer.printText({ content: '样式: ' + (order.value.Styles || '(无样式)')+'\n' })
|
||||||
|
//printer.printLine({ line_length: 1 })
|
||||||
|
|
||||||
|
// 第四行:创建日期
|
||||||
|
printer.printText({ content: '日期: ' + formatDate(order.value.CreatedAt) })
|
||||||
|
//printer.printLine({ line_length: 1 })
|
||||||
|
|
||||||
|
// 条形码:内容 po:ID,高度 4
|
||||||
|
printer.printBarcode({
|
||||||
|
text: 'po:' + orderId.value,
|
||||||
|
height: 40,
|
||||||
|
barcodeType: 73
|
||||||
|
})
|
||||||
|
|
||||||
|
printer.printGoToNextMark()
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function goEdit() {
|
||||||
|
uni.navigateTo({ url: `/pages/order/edit-order?id=${orderId.value}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToWorkOrder(id) {
|
||||||
|
uni.navigateTo({ url: `/pages/workorder/show-workorder?id=${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLink() {
|
||||||
|
if (!order.value?.Link) return
|
||||||
|
let url = order.value.Link.trim()
|
||||||
|
if (!/^https?:\/\//i.test(url)) url = 'https://' + url
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: url,
|
||||||
|
success: () => uni.showToast({ title: '链接已复制', icon: 'success' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRefresh() {
|
||||||
|
refreshing.value = true
|
||||||
|
await fetchOrderDetail()
|
||||||
|
refreshing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
if (options?.id) {
|
||||||
|
orderId.value = parseInt(options.id)
|
||||||
|
fetchOrderDetail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 每次页面显示时刷新数据
|
||||||
|
onShow(() => {
|
||||||
|
if (orderId.value) {
|
||||||
|
fetchOrderDetail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container { min-height: 100vh; background-color: #f5f5f5; padding-bottom: 20rpx; }
|
||||||
|
.header { background-color: #fff; padding: 30rpx; display: flex; align-items: center; }
|
||||||
|
.back-btn { font-size: 32rpx; color: #007AFF; margin-right: 20rpx; }
|
||||||
|
.title { font-size: 36rpx; font-weight: bold; color: #333; flex: 1; text-align: center; }
|
||||||
|
.header-actions { display: flex; align-items: center; gap: 20rpx; }
|
||||||
|
.print-btn { font-size: 36rpx; color: #007AFF; }
|
||||||
|
.edit-btn { font-size: 28rpx; color: #007AFF; }
|
||||||
|
.header-right { width: 60rpx; }
|
||||||
|
.content { padding: 20rpx; height: calc(100vh - 120rpx); }
|
||||||
|
.loading, .empty { display: flex; justify-content: center; align-items: center; height: 300rpx; color: #999; }
|
||||||
|
.card { background-color: #fff; border-radius: 16rpx; margin-bottom: 20rpx; overflow: hidden; }
|
||||||
|
.card-header { padding: 24rpx 30rpx; border-bottom: 1rpx solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.card-title { font-size: 30rpx; font-weight: bold; color: #333; }
|
||||||
|
.order-status { font-size: 24rpx; padding: 8rpx 20rpx; border-radius: 20rpx; }
|
||||||
|
.order-status.pending, .timeline-status.pending, .timeline-dot.pending { background-color: #fff7e6; color: #faad14; }
|
||||||
|
.order-status.ordered, .timeline-status.ordered, .timeline-dot.ordered { background-color: #e6f7ff; color: #1890ff; }
|
||||||
|
.order-status.arrived, .timeline-status.arrived, .timeline-dot.arrived { background-color: #f9f0ff; color: #722ed1; }
|
||||||
|
.order-status.received, .timeline-status.received, .timeline-dot.received { background-color: #f6ffed; color: #52c41a; }
|
||||||
|
.order-status.lost, .timeline-status.lost, .timeline-dot.lost { background-color: #fff1f0; color: #ff4d4f; }
|
||||||
|
.order-status.returned, .timeline-status.returned, .timeline-dot.returned { background-color: #f5f5f5; color: #999; }
|
||||||
|
.info-row { padding: 20rpx 30rpx; display: flex; border-bottom: 1rpx solid #f0f0f0; }
|
||||||
|
.info-label { width: 160rpx; color: #999; font-size: 28rpx; flex-shrink: 0; }
|
||||||
|
.info-value { flex: 1; color: #333; font-size: 28rpx; word-break: break-all; }
|
||||||
|
.info-value.link { color: #007AFF; }
|
||||||
|
.status-buttons { display: flex; flex-wrap: wrap; padding: 20rpx; gap: 20rpx; }
|
||||||
|
.status-btn { padding: 16rpx 30rpx; border-radius: 30rpx; background-color: #f5f5f5; color: #666; font-size: 26rpx; }
|
||||||
|
.status-btn.active { color: #fff; }
|
||||||
|
.status-btn.active.pending { background-color: #faad14; }
|
||||||
|
.status-btn.active.ordered { background-color: #1890ff; }
|
||||||
|
.status-btn.active.arrived { background-color: #722ed1; }
|
||||||
|
.status-btn.active.received { background-color: #52c41a; }
|
||||||
|
.status-btn.active.lost { background-color: #ff4d4f; }
|
||||||
|
.status-btn.active.returned { background-color: #999; }
|
||||||
|
.cost-table { padding: 0 20rpx 20rpx; }
|
||||||
|
.cost-header, .cost-row { display: flex; padding: 20rpx 10rpx; }
|
||||||
|
.cost-header { background-color: #fafafa; border-radius: 8rpx; }
|
||||||
|
.cost-th, .cost-td { flex: 1; text-align: center; font-size: 24rpx; }
|
||||||
|
.cost-th { color: #999; font-weight: 500; }
|
||||||
|
.cost-total { padding: 20rpx; border-top: 1rpx solid #f0f0f0; display: flex; gap: 20rpx; }
|
||||||
|
.total-tag { background-color: #e6f7ff; color: #1890ff; padding: 8rpx 20rpx; border-radius: 8rpx; font-size: 24rpx; }
|
||||||
|
.photo-grid { display: flex; flex-wrap: wrap; padding: 20rpx; gap: 20rpx; }
|
||||||
|
.photo-item { width: 200rpx; height: 200rpx; border-radius: 12rpx; overflow: hidden; }
|
||||||
|
.photo-item image { width: 100%; height: 100%; }
|
||||||
|
.workorder-item { padding: 24rpx 30rpx; border-bottom: 1rpx solid #f0f0f0; display: flex; align-items: center; }
|
||||||
|
.wo-id { color: #999; font-size: 26rpx; margin-right: 16rpx; }
|
||||||
|
.wo-title { flex: 1; color: #333; font-size: 28rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.wo-status { font-size: 24rpx; padding: 8rpx 16rpx; border-radius: 16rpx; margin-left: 16rpx; }
|
||||||
|
.wo-status.pending { background-color: #fff7e6; color: #faad14; }
|
||||||
|
.wo-status.checked { background-color: #e6f7ff; color: #1890ff; }
|
||||||
|
.wo-status.parts_ordered { background-color: #f9f0ff; color: #722ed1; }
|
||||||
|
.wo-status.repaired { background-color: #f6ffed; color: #52c41a; }
|
||||||
|
.wo-status.returned { background-color: #f5f5f5; color: #999; }
|
||||||
|
.wo-status.unrepairable { background-color: #fff1f0; color: #ff4d4f; }
|
||||||
|
.timeline { padding: 20rpx 30rpx; }
|
||||||
|
.timeline-item { display: flex; padding-bottom: 30rpx; position: relative; }
|
||||||
|
.timeline-item:not(:last-child)::before { content: ''; position: absolute; left: 10rpx; top: 30rpx; bottom: 0; width: 2rpx; background-color: #e5e5e5; }
|
||||||
|
.timeline-dot { width: 20rpx; height: 20rpx; border-radius: 50%; margin-top: 8rpx; flex-shrink: 0; margin-right: 20rpx; }
|
||||||
|
.timeline-content { flex: 1; }
|
||||||
|
.timeline-header { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.timeline-date { font-size: 24rpx; color: #999; }
|
||||||
|
.timeline-user { display: block; font-size: 24rpx; color: #999; margin-top: 8rpx; }
|
||||||
|
.timeline-comment { display: block; font-size: 28rpx; color: #333; margin-top: 12rpx; }
|
||||||
|
.timeline-photos { display: flex; flex-wrap: wrap; gap: 16rpx; margin-top: 16rpx; }
|
||||||
|
.timeline-photo { width: 100rpx; height: 100rpx; border-radius: 8rpx; }
|
||||||
|
.modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: flex-end; z-index: 999; }
|
||||||
|
.modal-content { width: 100%; background-color: #fff; border-radius: 24rpx 24rpx 0 0; max-height: 80vh; }
|
||||||
|
.modal-header { padding: 30rpx; display: flex; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #f0f0f0; }
|
||||||
|
.modal-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
||||||
|
.modal-close { font-size: 48rpx; color: #999; line-height: 1; }
|
||||||
|
.modal-body { padding: 30rpx; max-height: 60vh; overflow-y: auto; }
|
||||||
|
.form-item { margin-bottom: 30rpx; }
|
||||||
|
.form-label { display: block; font-size: 28rpx; color: #666; margin-bottom: 16rpx; }
|
||||||
|
.status-display { font-size: 32rpx; color: #333; font-weight: 500; }
|
||||||
|
.form-textarea { width: 100%; height: 160rpx; border: 1rpx solid #e5e5e5; border-radius: 12rpx; padding: 20rpx; font-size: 28rpx; box-sizing: border-box; }
|
||||||
|
.photo-upload-area { display: flex; flex-wrap: wrap; gap: 20rpx; }
|
||||||
|
.uploaded-photo { width: 150rpx; height: 150rpx; border-radius: 12rpx; overflow: hidden; position: relative; }
|
||||||
|
.uploaded-photo image { width: 100%; height: 100%; }
|
||||||
|
.photo-remove { position: absolute; top: -10rpx; right: -10rpx; width: 40rpx; height: 40rpx; background-color: rgba(0, 0, 0, 0.5); color: #fff; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 28rpx; }
|
||||||
|
.add-photo-btn { width: 150rpx; height: 150rpx; border-radius: 12rpx; border: 2rpx dashed #ddd; display: flex; justify-content: center; align-items: center; }
|
||||||
|
.add-photo-btn text { font-size: 60rpx; color: #999; }
|
||||||
|
.modal-footer { padding: 30rpx; display: flex; gap: 20rpx; border-top: 1rpx solid #f0f0f0; }
|
||||||
|
.btn-cancel, .btn-confirm { flex: 1; text-align: center; padding: 24rpx; border-radius: 12rpx; font-size: 30rpx; }
|
||||||
|
.btn-cancel { background-color: #f5f5f5; color: #666; }
|
||||||
|
.btn-confirm { background-color: #007AFF; color: #fff; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="header">
|
||||||
|
<text class="title">采购订单</text>
|
||||||
|
<view class="add-btn" @click="goToAdd">
|
||||||
|
<text>+ 新增</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="stats-row">
|
||||||
|
<view class="stat-item" @click="filterStatus('pending')">
|
||||||
|
<text class="stat-num">{{ stats.pending || 0 }}</text>
|
||||||
|
<text class="stat-label">待处理</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item" @click="filterStatus('ordered')">
|
||||||
|
<text class="stat-num">{{ stats.ordered || 0 }}</text>
|
||||||
|
<text class="stat-label">已下单</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item" @click="filterStatus('arrived')">
|
||||||
|
<text class="stat-num">{{ stats.arrived || 0 }}</text>
|
||||||
|
<text class="stat-label">已到达</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item" @click="filterStatus('received')">
|
||||||
|
<text class="stat-num">{{ stats.received || 0 }}</text>
|
||||||
|
<text class="stat-label">已收件</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-bar">
|
||||||
|
<view class="search-box">
|
||||||
|
<input
|
||||||
|
class="search-input"
|
||||||
|
placeholder="搜索订单..."
|
||||||
|
v-model="searchText"
|
||||||
|
@confirm="onSearch"
|
||||||
|
/>
|
||||||
|
<text class="search-btn" @click="onSearch">搜索</text>
|
||||||
|
</view>
|
||||||
|
<picker mode="selector" :range="statusOptions" range-key="label" @change="onStatusChange">
|
||||||
|
<view class="picker">
|
||||||
|
{{ currentFilter.label || '全部状态' }}
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="order-list" refresher-enabled @refresherrefresh="onRefresh" :refresher-triggered="refreshing" @scrolltolower="loadMore">
|
||||||
|
<view v-if="loading && orders.length === 0" class="loading">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="orders.length === 0" class="empty">
|
||||||
|
<text>暂无订单</text>
|
||||||
|
</view>
|
||||||
|
<view v-else>
|
||||||
|
<view
|
||||||
|
v-for="item in orders"
|
||||||
|
:key="item.ID"
|
||||||
|
class="order-card"
|
||||||
|
@click="goDetail(item.ID)"
|
||||||
|
>
|
||||||
|
<view class="order-header">
|
||||||
|
<text class="order-id">#{{ item.ID }}</text>
|
||||||
|
<text class="order-status" :class="item.OrderStatus">{{ getStatusText(item.OrderStatus) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-content">
|
||||||
|
<text class="order-title">{{ item.Title || '无标题' }}</text>
|
||||||
|
<text class="order-remark" v-if="item.Remark">{{ item.Remark }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-footer">
|
||||||
|
<text class="order-date">{{ formatDate(item.CreatedAt) }}</text>
|
||||||
|
<text class="order-link" v-if="item.Link">有链接</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loadingMore" class="loading-more">
|
||||||
|
<text>加载更多...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="hasMore" class="load-more-btn" @click="loadMore">
|
||||||
|
<text>加载更多</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { purchaseApi } from '@/api/purchase.js'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadingMore = ref(false)
|
||||||
|
const orders = ref([])
|
||||||
|
const stats = ref({})
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const hasMore = ref(false)
|
||||||
|
const currentFilter = ref({})
|
||||||
|
const searchText = ref('')
|
||||||
|
const refreshing = ref(false)
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: '', label: '全部状态' },
|
||||||
|
{ value: 'pending', label: '待处理' },
|
||||||
|
{ value: 'ordered', label: '已下单' },
|
||||||
|
{ value: 'arrived', label: '已到达' },
|
||||||
|
{ value: 'received', label: '已收件' },
|
||||||
|
{ value: 'lost', label: '丢件' },
|
||||||
|
{ value: 'returned', label: '退件' }
|
||||||
|
]
|
||||||
|
|
||||||
|
function getStatusText(status) {
|
||||||
|
const option = statusOptions.find(s => s.value === status)
|
||||||
|
return option ? option.label : status
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchStats() {
|
||||||
|
try {
|
||||||
|
const res = await purchaseApi.getOrderCount()
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
stats.value = res.data
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取订单统计失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchOrders(reset = false) {
|
||||||
|
if (reset) {
|
||||||
|
page.value = 1
|
||||||
|
orders.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading.value || loadingMore.value) return
|
||||||
|
if (!reset && !hasMore.value) return
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
loading.value = true
|
||||||
|
} else {
|
||||||
|
loadingMore.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
entries: pageSize.value,
|
||||||
|
page: page.value
|
||||||
|
}
|
||||||
|
if (currentFilter.value.value) {
|
||||||
|
params.status = currentFilter.value.value
|
||||||
|
}
|
||||||
|
if (searchText.value) {
|
||||||
|
params.search = searchText.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await purchaseApi.getOrders(params)
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
const list = res.data.all_orders || []
|
||||||
|
if (reset) {
|
||||||
|
orders.value = list
|
||||||
|
} else {
|
||||||
|
orders.value = [...orders.value, ...list]
|
||||||
|
}
|
||||||
|
hasMore.value = list.length >= pageSize.value
|
||||||
|
page.value++
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取订单列表失败', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStatusChange(e) {
|
||||||
|
currentFilter.value = statusOptions[e.detail.value]
|
||||||
|
fetchOrders(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterStatus(status) {
|
||||||
|
currentFilter.value = statusOptions.find(s => s.value === status) || statusOptions[0]
|
||||||
|
fetchOrders(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() {
|
||||||
|
fetchOrders(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMore() {
|
||||||
|
if (hasMore.value && !loadingMore.value) {
|
||||||
|
fetchOrders(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDetail(id) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/order/order-detail?id=${id}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStats()
|
||||||
|
fetchOrders(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听状态变更后刷新
|
||||||
|
uni.$on('purchase-refresh', () => {
|
||||||
|
fetchStats()
|
||||||
|
fetchOrders(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
function goToAdd() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/order/order-add'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRefresh() {
|
||||||
|
refreshing.value = true
|
||||||
|
await Promise.all([fetchStats(), fetchOrders(true)])
|
||||||
|
refreshing.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
padding: 12rpx 24rpx;
|
||||||
|
background-color: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 26rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
display: flex;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20rpx 10rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 15rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num {
|
||||||
|
display: block;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #007AFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 15rpx 25rpx;
|
||||||
|
border-radius: 10rpx 0 0 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
background-color: #007AFF;
|
||||||
|
color: #fff;
|
||||||
|
padding: 15rpx 35rpx;
|
||||||
|
border-radius: 0 10rpx 10rpx 0;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list {
|
||||||
|
height: calc(100vh - 480rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading, .empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 0 20rpx 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-id {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status {
|
||||||
|
font-size: 22rpx;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.pending {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.ordered {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.arrived {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.received {
|
||||||
|
background-color: #e0f2f1;
|
||||||
|
color: #009688;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.lost {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.returned {
|
||||||
|
background-color: #f3e5f5;
|
||||||
|
color: #9c27b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-content {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-remark {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-top: 1rpx solid #f0f0f0;
|
||||||
|
padding-top: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-date {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #007AFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more, .load-more-btn {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
color: #007AFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-btn {
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 0 20rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="group">
|
||||||
|
<text class="group-title">打印机控制</text>
|
||||||
|
<view class="group-body">
|
||||||
|
<view class="cell" @tap="initPrinter">
|
||||||
|
<text class="cell-label">初始化打印</text>
|
||||||
|
</view>
|
||||||
|
<view class="cell" @tap="closePrinter">
|
||||||
|
<text class="cell-label">关闭打印</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="group">
|
||||||
|
<text class="group-title">打印测试</text>
|
||||||
|
<view class="group-body">
|
||||||
|
<view class="cell" @tap="printerText">
|
||||||
|
<text class="cell-label">打印小票</text>
|
||||||
|
</view>
|
||||||
|
<view class="cell" @tap="printerLabel">
|
||||||
|
<text class="cell-label">测试打印(文字+二维码)</text>
|
||||||
|
</view>
|
||||||
|
<view class="cell" @tap="printLabel">
|
||||||
|
<text class="cell-label">打印标签(条形码)</text>
|
||||||
|
</view>
|
||||||
|
<view class="cell" @tap="myprinttest">
|
||||||
|
<text class="cell-label">我的demo</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 日志区域 -->
|
||||||
|
<view class="log-box" v-if="logs.length">
|
||||||
|
<text class="group-title">日志</text>
|
||||||
|
<view class="log-content">
|
||||||
|
<text class="log-line" v-for="(log, i) in logs" :key="i">{{ log }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="btn-wrapper">
|
||||||
|
<button class="clear-btn" @tap="logs = []">清空日志</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
|
const printer = uni.requireNativePlugin('LcPrinter')
|
||||||
|
const modal = uni.requireNativePlugin('modal')
|
||||||
|
const globalEvent = uni.requireNativePlugin('globalEvent')
|
||||||
|
|
||||||
|
const logs = ref([])
|
||||||
|
|
||||||
|
function log(msg) {
|
||||||
|
logs.value.push(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
// 添加打印状态监听
|
||||||
|
globalEvent.addEventListener('onPrintCallback', (e) => {
|
||||||
|
log('state: ' + JSON.stringify(e))
|
||||||
|
uni.showToast({
|
||||||
|
title: 'state: ' + JSON.stringify(e),
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
if (e.key == 0) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '打印成功',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
} else if (e.key == 3) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '缺纸',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 打印机版本获取回调
|
||||||
|
globalEvent.addEventListener('onVersion', (e) => {
|
||||||
|
log('version: ' + JSON.stringify(e))
|
||||||
|
uni.showToast({
|
||||||
|
title: 'version: ' + JSON.stringify(e),
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
globalEvent.addEventListener('getsupportprint', (e) => {
|
||||||
|
log('getsupportprint: ' + JSON.stringify(e))
|
||||||
|
uni.showToast({
|
||||||
|
title: 'key: ' + JSON.stringify(e),
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function initPrinter() {
|
||||||
|
console.log('初始化')
|
||||||
|
log('初始化打印机...')
|
||||||
|
const ret = printer.initPrinter({})
|
||||||
|
modal.toast({
|
||||||
|
message: ret,
|
||||||
|
duration: 1.5
|
||||||
|
})
|
||||||
|
printer.setConcentration({
|
||||||
|
level: 39
|
||||||
|
})
|
||||||
|
printer.setLineSpacing({
|
||||||
|
spacing: 1
|
||||||
|
})
|
||||||
|
printer.getsupportprint()
|
||||||
|
log('初始化完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePrinter() {
|
||||||
|
console.log('关闭')
|
||||||
|
log('关闭打印机')
|
||||||
|
printer.closePrinter()
|
||||||
|
}
|
||||||
|
|
||||||
|
function printerLabel() {
|
||||||
|
console.log('测试打印')
|
||||||
|
log('测试打印(文字+二维码)...')
|
||||||
|
|
||||||
|
// 标签打印,使用黑标
|
||||||
|
printer.printEnableMark({
|
||||||
|
enable: true
|
||||||
|
})
|
||||||
|
|
||||||
|
printer.setFontSize({
|
||||||
|
fontSize: 0
|
||||||
|
})
|
||||||
|
printer.setTextBold({
|
||||||
|
bold: true
|
||||||
|
})
|
||||||
|
|
||||||
|
printer.printText({
|
||||||
|
content: '垃圾收运小票凭证'
|
||||||
|
})
|
||||||
|
printer.printLine({
|
||||||
|
line_length: 1
|
||||||
|
})
|
||||||
|
printer.printText({
|
||||||
|
content: 'asdads'
|
||||||
|
})
|
||||||
|
printer.printLine({
|
||||||
|
line_length: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('测试打印QR')
|
||||||
|
log('打印二维码...')
|
||||||
|
printer.printQR({
|
||||||
|
text: 'title',
|
||||||
|
height: 400,
|
||||||
|
offset: 1
|
||||||
|
})
|
||||||
|
printer.printLine({
|
||||||
|
line_length: 2
|
||||||
|
})
|
||||||
|
|
||||||
|
printer.start()
|
||||||
|
console.log('测试打印QR结束')
|
||||||
|
log('打印完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
function printerText() {
|
||||||
|
console.log('打印小票')
|
||||||
|
log('打印小票...')
|
||||||
|
|
||||||
|
// 普通打印(小票),不使用黑标
|
||||||
|
printer.printEnableMark({
|
||||||
|
enable: false
|
||||||
|
})
|
||||||
|
|
||||||
|
printer.setFontSize({
|
||||||
|
fontSize: 0
|
||||||
|
})
|
||||||
|
printer.setTextBold({
|
||||||
|
bold: true
|
||||||
|
})
|
||||||
|
|
||||||
|
printer.printText({
|
||||||
|
content: '这是一张测试小票'
|
||||||
|
})
|
||||||
|
printer.setTextBold({
|
||||||
|
bold: false
|
||||||
|
})
|
||||||
|
printer.printLine({
|
||||||
|
line_length: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
printer.printText({
|
||||||
|
content: 'commodity'
|
||||||
|
})
|
||||||
|
printer.printLine({
|
||||||
|
line_length: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
printer.printText({
|
||||||
|
content: 'Quantity'
|
||||||
|
})
|
||||||
|
printer.printLine({
|
||||||
|
line_length: 1
|
||||||
|
})
|
||||||
|
printer.printText({
|
||||||
|
content: 'unit price'
|
||||||
|
})
|
||||||
|
printer.printLine({
|
||||||
|
line_length: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
printer.printBarcode({
|
||||||
|
text: '123456',
|
||||||
|
height: 80,
|
||||||
|
barcodeType: 73
|
||||||
|
})
|
||||||
|
printer.printLine({
|
||||||
|
line_length: 1
|
||||||
|
})
|
||||||
|
printer.printQR({
|
||||||
|
text: '1234456'
|
||||||
|
})
|
||||||
|
printer.printLine({
|
||||||
|
line_length: 2
|
||||||
|
})
|
||||||
|
printer.start()
|
||||||
|
log('小票打印完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
function printLabel() {
|
||||||
|
log('打印标签(条形码)...')
|
||||||
|
|
||||||
|
// 标签打印,使用黑标
|
||||||
|
printer.printEnableMark({
|
||||||
|
enable: true
|
||||||
|
})
|
||||||
|
|
||||||
|
printer.printBarcode({
|
||||||
|
text: '1234567890123456789',
|
||||||
|
height: 80,
|
||||||
|
barcodeType: 73
|
||||||
|
})
|
||||||
|
printer.printLine({
|
||||||
|
line_length: 5
|
||||||
|
})
|
||||||
|
printer.printGoToNextMark()
|
||||||
|
log('标签打印完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
function myprinttest(){
|
||||||
|
// 标签打印,使用黑标
|
||||||
|
printer.printEnableMark({
|
||||||
|
enable: true
|
||||||
|
})
|
||||||
|
printer.printText({
|
||||||
|
content: 'type:1\n123'
|
||||||
|
})
|
||||||
|
printer.printBarcode({
|
||||||
|
text: 'abcdefg:1234',
|
||||||
|
height: 40,
|
||||||
|
barcodeType: 73,
|
||||||
|
|
||||||
|
})
|
||||||
|
printer.printGoToNextMark()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f2f2f7;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
}
|
||||||
|
.group {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
.group-title {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #8e8e93;
|
||||||
|
padding: 0 32rpx 12rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.group-body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
margin: 0 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-bottom: 1rpx solid #e5e5ea;
|
||||||
|
}
|
||||||
|
.cell:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.cell:active {
|
||||||
|
background-color: #f2f2f7;
|
||||||
|
}
|
||||||
|
.cell-label {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #1c1c1e;
|
||||||
|
}
|
||||||
|
.btn-wrapper {
|
||||||
|
padding: 20rpx 32rpx;
|
||||||
|
}
|
||||||
|
.clear-btn {
|
||||||
|
background-color: #ff3b30;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 12rpx 0;
|
||||||
|
}
|
||||||
|
.log-box {
|
||||||
|
margin: 0 24rpx 32rpx;
|
||||||
|
background: #1c1c1e;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
.log-content {
|
||||||
|
max-height: 400rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.log-line {
|
||||||
|
display: block;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #32d74b;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,507 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<view class="search-bar">
|
||||||
|
<text class="search-back" @click="goBack">‹</text>
|
||||||
|
<input
|
||||||
|
class="search-input"
|
||||||
|
v-model="keyword"
|
||||||
|
placeholder="输入关键词搜索..."
|
||||||
|
confirm-type="search"
|
||||||
|
@input="onInput"
|
||||||
|
@confirm="doSearch"
|
||||||
|
/>
|
||||||
|
<text class="search-scan" @click="onScan">📷</text>
|
||||||
|
<text class="search-clear" v-if="keyword" @click="clearKeyword">×</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 分类Tab -->
|
||||||
|
<view class="tabs">
|
||||||
|
<view
|
||||||
|
class="tab"
|
||||||
|
v-for="cat in categories"
|
||||||
|
:key="cat.key"
|
||||||
|
:class="{ active: currentCat === cat.key }"
|
||||||
|
@click="switchCat(cat.key)"
|
||||||
|
>
|
||||||
|
{{ cat.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view v-if="loading" class="loading">
|
||||||
|
<text>搜索中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-else-if="results.length === 0 && searched" class="empty">
|
||||||
|
<text>未找到相关内容</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 搜索结果列表 -->
|
||||||
|
<scroll-view v-else scroll-y class="result-list">
|
||||||
|
<view
|
||||||
|
v-for="item in results"
|
||||||
|
:key="item._type + ':' + item.ID"
|
||||||
|
class="result-card"
|
||||||
|
@click="goDetail(item)"
|
||||||
|
>
|
||||||
|
<view class="result-icon">
|
||||||
|
<text>{{ getIcon(item._type) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="result-info">
|
||||||
|
<text class="result-title">{{ getTitle(item) }}</text>
|
||||||
|
<text class="result-desc">{{ getDesc(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="result-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { purchaseApi } from '@/api/purchase.js'
|
||||||
|
import { warehouseApi } from '@/api/warehouse.js'
|
||||||
|
import { workOrderApi } from '@/api/work_order.js'
|
||||||
|
|
||||||
|
const keyword = ref('')
|
||||||
|
const currentCat = ref('all')
|
||||||
|
const loading = ref(false)
|
||||||
|
const results = ref([])
|
||||||
|
const searched = ref(false)
|
||||||
|
|
||||||
|
let searchTimer = null
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ key: 'all', label: '全部' },
|
||||||
|
{ key: 'order', label: '订单' },
|
||||||
|
{ key: 'workorder', label: '工单' },
|
||||||
|
{ key: 'item', label: '物品' },
|
||||||
|
{ key: 'container', label: '容器' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function onInput() {
|
||||||
|
if (searchTimer) clearTimeout(searchTimer)
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
if (keyword.value.trim()) {
|
||||||
|
doSearch()
|
||||||
|
} else {
|
||||||
|
results.value = []
|
||||||
|
searched.value = false
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSearch() {
|
||||||
|
if (!keyword.value.trim()) {
|
||||||
|
results.value = []
|
||||||
|
searched.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 条码识别:wo:ID / item:ID / warehouse:ID
|
||||||
|
const val = keyword.value.trim().toLowerCase()
|
||||||
|
let match
|
||||||
|
|
||||||
|
// wo:ID → 工单
|
||||||
|
match = val.match(/^wo:(\d+)$/)
|
||||||
|
if (match) {
|
||||||
|
uni.navigateTo({ url: '/pages/workorder/show-workorder?id=' + match[1] })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// item:ID → 物品
|
||||||
|
match = val.match(/^item:(\d+)$/)
|
||||||
|
if (match) {
|
||||||
|
uni.navigateTo({ url: '/pages/warehouse/item-detail?id=' + match[1] })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// warehouse:ID → 容器(tabBar页面,用事件传参)
|
||||||
|
match = val.match(/^warehouse:(\d+)$/)
|
||||||
|
if (match) {
|
||||||
|
uni.$emit('barcode-navigate-container', { id: match[1] })
|
||||||
|
uni.switchTab({ url: '/pages/warehouse/warehouse' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// po:ID → 采购订单
|
||||||
|
match = val.match(/^po:(\d+)$/)
|
||||||
|
if (match) {
|
||||||
|
uni.navigateTo({ url: '/pages/order/order-detail?id=' + match[1] })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
searched.value = true
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const cat = currentCat.value
|
||||||
|
const tasks = []
|
||||||
|
|
||||||
|
if (cat === 'all' || cat === 'order') {
|
||||||
|
tasks.push(
|
||||||
|
purchaseApi.getOrders({ search: keyword.value, entries: 50, page: 1 }).then(res => {
|
||||||
|
if (res.errCode === 0 && res.data && res.data.all_orders) {
|
||||||
|
return (res.data.all_orders || []).map(o => ({ ...o, _type: 'order' }))
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}).catch(() => [])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cat === 'all' || cat === 'workorder') {
|
||||||
|
tasks.push(
|
||||||
|
workOrderApi.list({ search: keyword.value, entries: 50, page: 1 }).then(res => {
|
||||||
|
if (res.errCode === 0 && res.data && res.data.all_orders) {
|
||||||
|
return (res.data.all_orders || []).map(o => ({ ...o, _type: 'workorder' }))
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}).catch(() => [])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cat === 'all' || cat === 'item') {
|
||||||
|
tasks.push(
|
||||||
|
warehouseApi.listItem({ search: keyword.value, entries: 50, page: 1 }).then(res => {
|
||||||
|
if (res.errCode === 0 && res.data && res.data.items) {
|
||||||
|
return (res.data.items || []).map(i => ({ ...i, _type: 'item' }))
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}).catch(() => [])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cat === 'all' || cat === 'container') {
|
||||||
|
tasks.push(
|
||||||
|
warehouseApi.listContainer({ search: keyword.value, entries: 50, page: 1 }).then(res => {
|
||||||
|
if (res.errCode === 0 && res.data && res.data.containers) {
|
||||||
|
return (res.data.containers || []).map(c => ({ ...c, _type: 'container' }))
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}).catch(() => [])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(tasks).then(allResults => {
|
||||||
|
const merged = allResults.flat()
|
||||||
|
results.value = merged
|
||||||
|
}).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchCat(cat) {
|
||||||
|
currentCat.value = cat
|
||||||
|
if (keyword.value.trim()) {
|
||||||
|
doSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearKeyword() {
|
||||||
|
keyword.value = ''
|
||||||
|
results.value = []
|
||||||
|
searched.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIcon(type) {
|
||||||
|
const icons = {
|
||||||
|
order: '📦',
|
||||||
|
workorder: '🔧',
|
||||||
|
item: '📦',
|
||||||
|
container: '📁',
|
||||||
|
}
|
||||||
|
return icons[type] || '📄'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTitle(item) {
|
||||||
|
switch (item._type) {
|
||||||
|
case 'order':
|
||||||
|
return item.Title || '订单'
|
||||||
|
case 'workorder':
|
||||||
|
return item.Title || '工单'
|
||||||
|
case 'item':
|
||||||
|
return item.Name || '物品'
|
||||||
|
case 'container':
|
||||||
|
return item.Title || '容器'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDesc(item) {
|
||||||
|
switch (item._type) {
|
||||||
|
case 'order':
|
||||||
|
return '状态: ' + (item.Status || '') + ' | ' + (item.Vendor || '')
|
||||||
|
case 'workorder':
|
||||||
|
return '状态: ' + (item.Status || '')
|
||||||
|
case 'item':
|
||||||
|
return item.SerialNumber || (item.Remark ? item.Remark.substring(0, 30) : '')
|
||||||
|
case 'container':
|
||||||
|
return '子容器: ' + (item.ChildCount || 0) + ' | 物品: ' + (item.ItemCount || 0)
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDetail(item) {
|
||||||
|
switch (item._type) {
|
||||||
|
case 'order':
|
||||||
|
uni.navigateTo({ url: '/pages/order/order-detail?id=' + item.ID })
|
||||||
|
break
|
||||||
|
case 'workorder':
|
||||||
|
uni.navigateTo({ url: '/pages/workorder/show-workorder?id=' + item.ID })
|
||||||
|
break
|
||||||
|
case 'item':
|
||||||
|
uni.navigateTo({ url: '/pages/warehouse/item-detail?id=' + item.ID })
|
||||||
|
break
|
||||||
|
case 'container':
|
||||||
|
uni.switchTab({ url: '/pages/warehouse/warehouse?container_id=' + item.ID })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
uni.navigateBack({ delta: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫码
|
||||||
|
function onScan() {
|
||||||
|
// #ifdef H5
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: 'H5 端不支持摄像头扫码,请手动输入条码(如 wo:1、item:1、warehouse:1)',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
// App 端使用 5+ API 检查并申请相机权限
|
||||||
|
const osName = plus.os.name
|
||||||
|
if (osName === 'Android') {
|
||||||
|
const main = plus.android.runtimeMainActivity()
|
||||||
|
const ContextCompat = plus.android.importClass('androidx.core.content.ContextCompat')
|
||||||
|
const Manifest = plus.android.importClass('android.Manifest$permission')
|
||||||
|
const permission = Manifest.CAMERA
|
||||||
|
const result = ContextCompat.checkSelfPermission(main, permission)
|
||||||
|
const PackageManager = plus.android.importClass('android.content.pm.PackageManager')
|
||||||
|
if (result !== PackageManager.PERMISSION_GRANTED) {
|
||||||
|
// 没有权限,申请权限
|
||||||
|
plus.android.requestPermissions(['android.permission.CAMERA'], (e) => {
|
||||||
|
if (e.deniedAlways.length > 0) {
|
||||||
|
// 永久拒绝,引导去设置
|
||||||
|
uni.showModal({
|
||||||
|
title: '需要相机权限',
|
||||||
|
content: '请在系统设置中开启相机权限后再使用扫码功能',
|
||||||
|
confirmText: '去设置',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
if (plus.os.name === 'Android') {
|
||||||
|
const Intent = plus.android.importClass('android.content.Intent')
|
||||||
|
const Settings = plus.android.importClass('android.provider.Settings')
|
||||||
|
const intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
const Uri = plus.android.importClass('android.net.Uri')
|
||||||
|
intent.setData(Uri.fromParts('package', main.getPackageName(), null))
|
||||||
|
main.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (e.granted.length > 0) {
|
||||||
|
// 授权成功,调用扫码
|
||||||
|
callScan()
|
||||||
|
}
|
||||||
|
}, (e) => {
|
||||||
|
console.error('申请权限失败', e)
|
||||||
|
callScan()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// iOS 或已有权限,直接调用扫码
|
||||||
|
callScan()
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
uni.getSetting({
|
||||||
|
success: (res) => {
|
||||||
|
if (res.authSetting['scope.camera'] === false) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '需要相机权限',
|
||||||
|
content: '扫码功能需要相机权限,请在设置中开启',
|
||||||
|
success: (modalRes) => {
|
||||||
|
if (modalRes.confirm) {
|
||||||
|
uni.openSetting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
callScan()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
callScan()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用扫码
|
||||||
|
function callScan() {
|
||||||
|
uni.scanCode({
|
||||||
|
onlyFromCamera: false,
|
||||||
|
scanType: ['barCode', 'qrCode'],
|
||||||
|
success: (res) => {
|
||||||
|
console.log('扫码结果:', res)
|
||||||
|
keyword.value = res.result
|
||||||
|
doSearch()
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('扫码失败', err)
|
||||||
|
if (err.errMsg && err.errMsg.includes('cancel')) return
|
||||||
|
uni.showToast({ title: '扫码失败: ' + (err.errMsg || '未知错误'), icon: 'none' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索栏 */
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
background-color: #007AFF;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-back {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #fff;
|
||||||
|
width: 60rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
height: 72rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #fff;
|
||||||
|
width: 60rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-scan {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #fff;
|
||||||
|
width: 60rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分类Tab */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
border-bottom: 1rpx solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 4rpx solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: #007AFF;
|
||||||
|
border-bottom-color: #007AFF;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载/空状态 */
|
||||||
|
.loading,
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 结果列表 */
|
||||||
|
.result-list {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-icon {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 36rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-info {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-arrow {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #ccc;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 页面根容器,灰色背景 -->
|
||||||
|
<view class="page">
|
||||||
|
|
||||||
|
<!-- ===== 分组:账号设置 ===== -->
|
||||||
|
<view class="group">
|
||||||
|
<!-- 分组标题 -->
|
||||||
|
<text class="group-title">base url</text>
|
||||||
|
<!-- 列表容器(白色卡片) -->
|
||||||
|
<view class="group-body">
|
||||||
|
<!-- 列表项:点击跳转或弹窗编辑 -->
|
||||||
|
<view class="cell" @tap="onEditApiUrl">
|
||||||
|
<text class="cell-value">{{ useConfig.getApiBaseUrl() || '未设置' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="btn-wrapper">
|
||||||
|
<button v-if="!isTesting" class="save-btn" @tap="onTest">测试连接</button>
|
||||||
|
<button v-if="isTesting" class="save-btn" disabled>正在测试...</button>
|
||||||
|
</view>
|
||||||
|
<view v-if="isTested&&!isTesting" :class="isTestok ? 'result success' : 'result error'">
|
||||||
|
{{ isTestok ? '✅ 连接成功' : '❌ 连接失败' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ===== 分组:打印机 ===== -->
|
||||||
|
<view class="group">
|
||||||
|
<text class="group-title">打印机</text>
|
||||||
|
<view class="group-body">
|
||||||
|
<view class="cell" @tap="goPrinterTest">
|
||||||
|
<text class="cell-label">打印机测试</text>
|
||||||
|
<text class="cell-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ===== 分组:关于 ===== -->
|
||||||
|
<view class="group">
|
||||||
|
<text class="group-title">关于</text>
|
||||||
|
<view class="group-body">
|
||||||
|
<view class="cell">
|
||||||
|
<text class="cell-label">版本号</text>
|
||||||
|
<text class="cell-value">{{ version }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { isUrl } from '@/utils/index.js'
|
||||||
|
import { useConfigStore } from '../../stores/config'
|
||||||
|
|
||||||
|
const useConfig=useConfigStore()
|
||||||
|
|
||||||
|
// ---- 版本号 ----
|
||||||
|
const version = ref('')
|
||||||
|
|
||||||
|
// ---- 响应式状态 ----
|
||||||
|
const isTested=ref(false)//是否点了测试
|
||||||
|
const isTesting=ref(false)//是否正在测试
|
||||||
|
const isTestok=ref(false)//测试是否通过
|
||||||
|
|
||||||
|
// ---- 事件处理 ----
|
||||||
|
|
||||||
|
function onTest(){
|
||||||
|
//console.log("test")
|
||||||
|
isTested.value=true
|
||||||
|
isTesting.value=true
|
||||||
|
uni.request({
|
||||||
|
url: useConfig.getApiBaseUrl(),
|
||||||
|
method: 'GET',
|
||||||
|
timeout:1000,
|
||||||
|
success: (res) => {
|
||||||
|
//console.log('成功', res.data)
|
||||||
|
if(res.data.return.isOpsApiRoot==true){
|
||||||
|
isTestok.value=true
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
//console.log('失败', err)
|
||||||
|
isTestok.value=false
|
||||||
|
},
|
||||||
|
complete() {
|
||||||
|
// 这个回调不管成功失败**一定**会执行
|
||||||
|
//console.log('请求完成')
|
||||||
|
isTesting.value=false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击 API URL → 弹出输入框(示例用 uni.showModal 简单实现)
|
||||||
|
function onEditApiUrl() {
|
||||||
|
// 实际可跳转子页面或弹出自定义弹窗
|
||||||
|
uni.showModal({
|
||||||
|
title: 'API Base URL',
|
||||||
|
editable: true,
|
||||||
|
placeholderText: 'https://example.com',
|
||||||
|
content: useConfig.getApiBaseUrl(),
|
||||||
|
success(res) {
|
||||||
|
if (res.confirm) {
|
||||||
|
console.log(res)
|
||||||
|
if (isUrl(res.content)){
|
||||||
|
useConfig.setApiBaseUrl(res.content)
|
||||||
|
console.log(res.content,":ok")
|
||||||
|
|
||||||
|
}else{
|
||||||
|
console.log(res.content,":not a url")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转打印机测试页
|
||||||
|
function goPrinterTest() {
|
||||||
|
uni.navigateTo({ url: '/pages/printer-test/printer-test' })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 真机/打包环境:通过 plus.runtime 获取
|
||||||
|
// #ifndef H5
|
||||||
|
version.value = plus.runtime.version || ''
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// H5 端:从 manifest.json 读取
|
||||||
|
// #ifdef H5
|
||||||
|
fetch('/manifest.json')
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(text => {
|
||||||
|
// manifest.json 可能含 JS 注释,需先去除再解析
|
||||||
|
const cleaned = text.replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, '')
|
||||||
|
const m = JSON.parse(cleaned)
|
||||||
|
version.value = (m.versionName || '') + ' (' + (m.versionCode || '') + ')'
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
version.value = ''
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 页面底色 */
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f2f2f7; /* iOS 系统级灰色 */
|
||||||
|
padding-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分组间距 */
|
||||||
|
.group {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分组标题:小号灰色,左侧缩进 */
|
||||||
|
.group-title {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #8e8e93;
|
||||||
|
padding: 0 32rpx 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 白色卡片容器 */
|
||||||
|
.group-body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
margin: 0 24rpx;
|
||||||
|
overflow: hidden; /* 保证圆角裁剪子项 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 每一行 cell */
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧标签 */
|
||||||
|
.cell-label {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #1c1c1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 点击高亮(uni-app 用 hover-class 实现,这里仅演示结构) */
|
||||||
|
.cell:active {
|
||||||
|
background-color: #f2f2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧标签 */
|
||||||
|
.cell-label {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #1c1c1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧容器(值 + 箭头) */
|
||||||
|
.cell-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧当前值 */
|
||||||
|
.cell-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #8e8e93;
|
||||||
|
max-width: 300rpx; /* 防止超长文本撑破布局 */
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右箭头 */
|
||||||
|
.cell-arrow {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #c7c7cc;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* switch 开关右对齐 */
|
||||||
|
.cell-switch {
|
||||||
|
transform: scale(0.85); /* 微调大小 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分割线:左侧缩进,不跨满 */
|
||||||
|
.divider {
|
||||||
|
height: 1rpx;
|
||||||
|
background-color: #e5e5ea;
|
||||||
|
margin-left: 32rpx; /* 左缩进,右边贴边,iOS 风格 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险操作行(退出登录) */
|
||||||
|
.cell-danger {
|
||||||
|
justify-content: center; /* 文字居中 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-label-danger {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #ff3b30; /* iOS 红色 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 顶部设置按钮 -->
|
||||||
|
<view class="header">
|
||||||
|
<text class="settings-icon" @click="goToSettings">⚙️</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 未登录状态 -->
|
||||||
|
<view v-if="!userStore.isLoggedIn" class="not-login">
|
||||||
|
<view class="avatar-placeholder">
|
||||||
|
<text class="avatar-icon">👤</text>
|
||||||
|
</view>
|
||||||
|
<text class="not-login-text">您还未登录</text>
|
||||||
|
<button class="login-btn" @click="goToLogin">前往登录</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 已登录状态 -->
|
||||||
|
<view v-else class="user-info">
|
||||||
|
<!-- 用户信息卡片 -->
|
||||||
|
<view class="user-card">
|
||||||
|
<view class="avatar-section">
|
||||||
|
<image
|
||||||
|
v-if="userStore.avatarUrl"
|
||||||
|
class="avatar-img"
|
||||||
|
:src="userStore.avatarUrl"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view v-else class="avatar-placeholder">
|
||||||
|
<text class="avatar-icon">👤</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="user-detail">
|
||||||
|
<text class="username">{{ userStore.username || '未设置昵称' }}</text>
|
||||||
|
<text class="user-role">{{ getUserRole() }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 功能菜单 -->
|
||||||
|
<view class="menu-section">
|
||||||
|
<view class="menu-item" @click="switchToTab('/pages/order/order')">
|
||||||
|
<text class="menu-icon">📦</text>
|
||||||
|
<text class="menu-text">我的订单</text>
|
||||||
|
<text class="menu-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-item" @click="switchToTab('/pages/warehouse/warehouse')">
|
||||||
|
<text class="menu-icon">🏭</text>
|
||||||
|
<text class="menu-text">仓库管理</text>
|
||||||
|
<text class="menu-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-item" @click="switchToTab('/pages/schedule/list')">
|
||||||
|
<text class="menu-icon">📅</text>
|
||||||
|
<text class="menu-text">我的日程</text>
|
||||||
|
<text class="menu-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 退出登录 -->
|
||||||
|
<view class="logout-section">
|
||||||
|
<button class="logout-btn" @click="handleLogout">退出登录</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import { useUserStore } from '../../stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 每次显示页面时检查登录状态
|
||||||
|
onShow(() => {
|
||||||
|
// 如果未登录,尝试从存储恢复会话
|
||||||
|
if (!userStore.isLoggedIn) {
|
||||||
|
userStore.restoreSession()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 跳转到登录页
|
||||||
|
const goToLogin = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/login/login'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到设置页
|
||||||
|
const goToSettings = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/settings/settings'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用跳转(支持普通页面和 TabBar 页面)
|
||||||
|
const navigateTo = (url) => {
|
||||||
|
// 检查是否是 TabBar 页面
|
||||||
|
const tabBarPages = ['/pages/index/index', '/pages/order/order', '/pages/warehouse/warehouse', '/pages/user/user']
|
||||||
|
if (tabBarPages.includes(url)) {
|
||||||
|
uni.switchTab({ url })
|
||||||
|
} else {
|
||||||
|
uni.navigateTo({ url })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到 TabBar 页面
|
||||||
|
const switchToTab = (url) => {
|
||||||
|
uni.switchTab({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户角色
|
||||||
|
const getUserRole = () => {
|
||||||
|
if (!userStore.user) return ''
|
||||||
|
// 根据用户组判断角色(这里简单处理)
|
||||||
|
return '用户'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const handleLogout = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认退出',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
confirmColor: '#FF3B30',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
userStore.logout()
|
||||||
|
uni.showToast({
|
||||||
|
title: '已退出登录',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #F5F5F5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部设置按钮 */
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 未登录状态 */
|
||||||
|
.not-login {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
border-radius: 80rpx;
|
||||||
|
background-color: #E0E0E0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-icon {
|
||||||
|
font-size: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-login-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 300rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
background-color: #007AFF;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 28rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已登录状态 */
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户信息卡片 */
|
||||||
|
.user-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-section {
|
||||||
|
margin-right: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-img {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 60rpx;
|
||||||
|
background-color: #E0E0E0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 功能菜单 */
|
||||||
|
.menu-section {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1rpx solid #F0F0F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-arrow {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #CCCCCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 退出登录 */
|
||||||
|
.logout-section {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 90rpx;
|
||||||
|
line-height: 90rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
color: #FF3B30;
|
||||||
|
font-size: 30rpx;
|
||||||
|
border-radius: 45rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="header">
|
||||||
|
<text class="back-btn" @click="goBack">‹ 返回</text>
|
||||||
|
<text class="title">新增物品</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="content">
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label required">名称 *</text>
|
||||||
|
<input class="form-input" v-model="form.name" placeholder="请输入物品名称" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">编号</text>
|
||||||
|
<input class="form-input" v-model="form.serialNumber" placeholder="请输入物品编号(选填)" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">数量</text>
|
||||||
|
<view class="quantity-row">
|
||||||
|
<view class="qty-btn" @click="form.quantity > 1 && form.quantity--">
|
||||||
|
<text class="qty-btn-text">−</text>
|
||||||
|
</view>
|
||||||
|
<input class="qty-input" type="number" v-model.number="form.quantity" />
|
||||||
|
<view class="qty-btn" @click="form.quantity++">
|
||||||
|
<text class="qty-btn-text">+</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">备注</text>
|
||||||
|
<textarea class="form-textarea" v-model="form.remark" placeholder="请输入备注(选填)" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关联客户 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">关联客户</text>
|
||||||
|
|
||||||
|
<!-- 已选中的客户 -->
|
||||||
|
<view v-if="selectedCustomers.length > 0" class="selected-customers">
|
||||||
|
<view
|
||||||
|
v-for="customer in selectedCustomers"
|
||||||
|
:key="customer.id"
|
||||||
|
class="customer-tag"
|
||||||
|
>
|
||||||
|
<text>{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</text>
|
||||||
|
<text class="customer-tag-remove" @click="removeCustomer(customer.id)">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<view class="customer-search-box">
|
||||||
|
<input
|
||||||
|
class="customer-search-input"
|
||||||
|
v-model="customerSearchQuery"
|
||||||
|
placeholder="搜索客户..."
|
||||||
|
@input="onCustomerSearchInput"
|
||||||
|
@focus="onCustomerSearchFocus"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 搜索结果 -->
|
||||||
|
<view v-if="showCustomerDropdown && customerSearchResults.length > 0" class="customer-dropdown">
|
||||||
|
<view
|
||||||
|
v-for="customer in customerSearchResults"
|
||||||
|
:key="customer.id"
|
||||||
|
class="customer-dropdown-item"
|
||||||
|
@click="selectCustomer(customer)"
|
||||||
|
>
|
||||||
|
<text class="customer-dropdown-name">{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</text>
|
||||||
|
<text v-if="customer.primary_phone" class="customer-dropdown-phone">{{ customer.primary_phone }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="showCustomerDropdown && customerSearchQuery && customerSearchResults.length === 0 && !customerSearchLoading" class="customer-dropdown-empty">
|
||||||
|
未找到匹配的客户
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">物品图片</text>
|
||||||
|
<view class="image-list">
|
||||||
|
<view
|
||||||
|
v-for="(photo, index) in form.photos"
|
||||||
|
:key="index"
|
||||||
|
class="image-item"
|
||||||
|
>
|
||||||
|
<image class="preview-img" :src="getPhotoUrl(photo)" mode="aspectFill" />
|
||||||
|
<view class="image-remove" @click="removePhoto(index)">×</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="form.photos.length < 9" class="image-add" @click="chooseImage">
|
||||||
|
<text class="add-icon">+</text>
|
||||||
|
<text class="add-text">添加图片</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="submit-section">
|
||||||
|
<view class="submit-btn" @click="submitForm">
|
||||||
|
<text>保存</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { warehouseApi } from '@/api/warehouse.js'
|
||||||
|
import { customerApi } from '@/api/customer.js'
|
||||||
|
import { useConfigStore } from '@/stores/config.js'
|
||||||
|
import api from '@/api/index.js'
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
// 容器ID(从URL参数获取)
|
||||||
|
const containerId = ref(0)
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const form = ref({
|
||||||
|
name: '',
|
||||||
|
serialNumber: '',
|
||||||
|
remark: '',
|
||||||
|
quantity: 1,
|
||||||
|
photos: []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 关联客户
|
||||||
|
const selectedCustomers = ref([])
|
||||||
|
const customerSearchQuery = ref('')
|
||||||
|
const customerSearchResults = ref([])
|
||||||
|
const showCustomerDropdown = ref(false)
|
||||||
|
const customerSearchLoading = ref(false)
|
||||||
|
let customerSearchTimer = null
|
||||||
|
|
||||||
|
function getPhotoUrl(sha256) {
|
||||||
|
const baseUrl = configStore.getFileBaseUrl ? configStore.getFileBaseUrl() : ''
|
||||||
|
return `${baseUrl}/api/files/get/${sha256}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseImage() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 9 - form.value.photos.length,
|
||||||
|
success: (res) => {
|
||||||
|
res.tempFilePaths.forEach(path => {
|
||||||
|
uploadImage(path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImage(filePath) {
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: '上传中...' })
|
||||||
|
const res = await api.upload('/files/upload/image', {
|
||||||
|
uri: filePath,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.errCode === 0 && res.data && res.data.hash) {
|
||||||
|
form.value.photos.push(res.data.hash)
|
||||||
|
uni.showToast({ title: '上传成功', icon: 'success' })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.message || '上传失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('上传失败', e)
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePhoto(index) {
|
||||||
|
form.value.photos.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户搜索
|
||||||
|
async function searchCustomers() {
|
||||||
|
if (!customerSearchQuery.value.trim()) {
|
||||||
|
customerSearchResults.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
customerSearchLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await customerApi.list(1, 20, customerSearchQuery.value)
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
customerSearchResults.value = res.data.customers || res.data || []
|
||||||
|
} else {
|
||||||
|
customerSearchResults.value = []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('搜索客户失败', e)
|
||||||
|
customerSearchResults.value = []
|
||||||
|
} finally {
|
||||||
|
customerSearchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCustomerSearchInput() {
|
||||||
|
clearTimeout(customerSearchTimer)
|
||||||
|
customerSearchTimer = setTimeout(() => {
|
||||||
|
searchCustomers()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCustomerSearchFocus() {
|
||||||
|
showCustomerDropdown.value = true
|
||||||
|
searchCustomers()
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCustomer(customer) {
|
||||||
|
const exists = selectedCustomers.value.find(c => c.id === customer.id)
|
||||||
|
if (!exists) {
|
||||||
|
selectedCustomers.value.push(customer)
|
||||||
|
}
|
||||||
|
customerSearchQuery.value = ''
|
||||||
|
showCustomerDropdown.value = false
|
||||||
|
customerSearchResults.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCustomer(id) {
|
||||||
|
const index = selectedCustomers.value.findIndex(c => c.id === id)
|
||||||
|
if (index >= 0) {
|
||||||
|
selectedCustomers.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForm() {
|
||||||
|
if (!form.value.name.trim()) {
|
||||||
|
uni.showToast({ title: '请输入名称', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: '保存中...' })
|
||||||
|
const data = {
|
||||||
|
name: form.value.name,
|
||||||
|
serial_number: form.value.serialNumber,
|
||||||
|
remark: form.value.remark,
|
||||||
|
quantity: form.value.quantity > 0 ? form.value.quantity : 1,
|
||||||
|
container_id: containerId.value || null,
|
||||||
|
photos: form.value.photos,
|
||||||
|
customer_ids: selectedCustomers.value.map(c => c.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await warehouseApi.addItem(data)
|
||||||
|
|
||||||
|
if (res.errCode === 0) {
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
goBack()
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.errMsg || '保存失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存失败', e)
|
||||||
|
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取页面参数
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
onLoad((options) => {
|
||||||
|
if (options && options.container_id) {
|
||||||
|
containerId.value = parseInt(options.container_id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #007AFF;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding-right: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: calc(100vh - 120rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 20rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label.required::after {
|
||||||
|
content: ' *';
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
min-height: 150rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片上传 */
|
||||||
|
.image-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item {
|
||||||
|
position: relative;
|
||||||
|
width: 180rpx;
|
||||||
|
height: 180rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0 0 0 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-add {
|
||||||
|
width: 180rpx;
|
||||||
|
height: 180rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 2rpx dashed #ddd;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-icon {
|
||||||
|
font-size: 60rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-section {
|
||||||
|
padding: 40rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background-color: #007AFF;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 数量选择器 */
|
||||||
|
.quantity-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-btn {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-btn-text {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-input {
|
||||||
|
width: 100rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-size: 30rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
margin: 0 16rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关联客户 */
|
||||||
|
.selected-customers {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
border: 1rpx solid #91d5ff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tag-remove {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tag-remove:active {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-box {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 72rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1rpx solid #eee;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
max-height: 400rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown-item:active {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown-phone {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown-empty {
|
||||||
|
padding: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,822 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="header">
|
||||||
|
<text class="back-btn" @click="goBack">‹ 返回</text>
|
||||||
|
<text class="title">物品详情</text>
|
||||||
|
<view class="header-right">
|
||||||
|
<text class="print-btn" @click="printItem">打印</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="content" refresher-enabled @refresherrefresh="onRefresh" :refresher-triggered="refreshing">
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view v-if="loading" class="loading">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 详情内容 -->
|
||||||
|
<view v-else-if="item">
|
||||||
|
<!-- 基本信息卡片 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="card-title">基本信息</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">名称</text>
|
||||||
|
<text class="info-value">{{ item.Name }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="item.SerialNumber">
|
||||||
|
<text class="info-label">编号</text>
|
||||||
|
<text class="info-value">{{ item.SerialNumber }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="item.Specification">
|
||||||
|
<text class="info-label">规格</text>
|
||||||
|
<text class="info-value">{{ item.Specification }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="item.ContainerBreadcrumb">
|
||||||
|
<text class="info-label">位置</text>
|
||||||
|
<text class="info-value location">{{ item.ContainerBreadcrumb }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="item.Remark">
|
||||||
|
<text class="info-label">备注</text>
|
||||||
|
<text class="info-value">{{ item.Remark }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">创建时间</text>
|
||||||
|
<text class="info-value">{{ formatDate(item.CreatedAt) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">更新时间</text>
|
||||||
|
<text class="info-value">{{ formatDate(item.UpdatedAt) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 图片卡片 -->
|
||||||
|
<view class="card" v-if="photos.length > 0">
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="card-title">图片 ({{ photos.length }})</text>
|
||||||
|
</view>
|
||||||
|
<view class="photo-grid">
|
||||||
|
<view
|
||||||
|
class="photo-item"
|
||||||
|
v-for="photo in photos"
|
||||||
|
:key="photo.ID"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="photo-img"
|
||||||
|
:src="getImageUrl(photo.Sha256)"
|
||||||
|
mode="aspectFill"
|
||||||
|
@click="previewAllImages(photo.Sha256)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 移动历史 -->
|
||||||
|
<view class="card" v-if="commits.length > 0">
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="card-title">移动历史</text>
|
||||||
|
</view>
|
||||||
|
<view class="timeline">
|
||||||
|
<view
|
||||||
|
class="timeline-item"
|
||||||
|
v-for="commit in commits"
|
||||||
|
:key="commit.ID"
|
||||||
|
>
|
||||||
|
<view class="timeline-dot"></view>
|
||||||
|
<view class="timeline-content">
|
||||||
|
<text class="timeline-text">
|
||||||
|
{{ commit.OldContainerBreadcrumb || '无' }} → {{ commit.NewContainerBreadcrumb || '已移除' }}
|
||||||
|
</text>
|
||||||
|
<text class="timeline-date">{{ formatDate(commit.CreatedAt) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关联工单 -->
|
||||||
|
<view class="card" v-if="workOrders.length > 0">
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="card-title">关联工单</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="workorder-item"
|
||||||
|
v-for="wo in workOrders"
|
||||||
|
:key="wo.ID"
|
||||||
|
@click="goToWorkOrder(wo.id)"
|
||||||
|
>
|
||||||
|
<text class="wo-id">#{{ wo.id }}</text>
|
||||||
|
<text class="wo-title">{{ wo.title }}</text>
|
||||||
|
<text class="wo-status" :class="wo.status">{{ getWorkOrderStatusText(wo.status) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关联客户 -->
|
||||||
|
<view class="card" v-if="linkedCustomers.length > 0">
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="card-title">关联客户</text>
|
||||||
|
</view>
|
||||||
|
<view class="linked-customers">
|
||||||
|
<view
|
||||||
|
class="linked-customer-item"
|
||||||
|
v-for="customer in linkedCustomers"
|
||||||
|
:key="customer.ID"
|
||||||
|
>
|
||||||
|
<text class="customer-name">{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</text>
|
||||||
|
<text v-if="customer.title" class="customer-title">{{ customer.title }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-else class="empty">
|
||||||
|
<text>未找到物品信息</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 底部操作栏 -->
|
||||||
|
<view class="action-bar" v-if="item">
|
||||||
|
<view class="action-btn workorder" @click="addWorkOrder">
|
||||||
|
<text>+ 工单</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-btn edit" @click="editItem">
|
||||||
|
<text>编辑</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-btn move" @click="openMoveModal">
|
||||||
|
<text>移动</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 移动弹窗 -->
|
||||||
|
<view class="modal" v-if="moveModalVisible" @click="closeMoveModal">
|
||||||
|
<view class="modal-content" @click.stop>
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">移动物品</text>
|
||||||
|
<text class="modal-close" @click="closeMoveModal">×</text>
|
||||||
|
</view>
|
||||||
|
<view class="modal-body">
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">选择容器</text>
|
||||||
|
<picker mode="selector" :range="containerOptions" range-key="Title" @change="onContainerChange">
|
||||||
|
<view class="picker">
|
||||||
|
{{ selectedContainer.Title || '请选择目标容器' }}
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="modal-footer">
|
||||||
|
<text class="btn-cancel" @click="closeMoveModal">取消</text>
|
||||||
|
<text class="btn-confirm" @click="confirmMove">确定</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { warehouseApi } from '@/api/warehouse.js'
|
||||||
|
import { useConfigStore } from '@/stores/config.js'
|
||||||
|
|
||||||
|
// 仅在 App 环境下加载原生插件
|
||||||
|
let printer = null
|
||||||
|
try {
|
||||||
|
if (uni.requireNativePlugin) {
|
||||||
|
printer = uni.requireNativePlugin('LcPrinter')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('打印机插件加载失败:', e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
const itemId = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const item = ref(null)
|
||||||
|
const photos = ref([])
|
||||||
|
const commits = ref([])
|
||||||
|
const workOrders = ref([])
|
||||||
|
const linkedCustomers = ref([])
|
||||||
|
const canModify = ref(false)
|
||||||
|
|
||||||
|
// 移动相关
|
||||||
|
const moveModalVisible = ref(false)
|
||||||
|
const containerOptions = ref([])
|
||||||
|
const selectedContainer = ref({})
|
||||||
|
const refreshing = ref(false)
|
||||||
|
|
||||||
|
const workOrderStatusOptions = [
|
||||||
|
{ value: 'pending', label: '待处理' },
|
||||||
|
{ value: 'checked', label: '已检查' },
|
||||||
|
{ value: 'parts_ordered', label: '已下单零件' },
|
||||||
|
{ value: 'repaired', label: '已维修' },
|
||||||
|
{ value: 'returned', label: '已送还' },
|
||||||
|
{ value: 'unrepairable', label: '无法维修' }
|
||||||
|
]
|
||||||
|
|
||||||
|
function getWorkOrderStatusText(status) {
|
||||||
|
const option = workOrderStatusOptions.find(s => s.value === status)
|
||||||
|
return option ? option.label : status
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageUrl(sha256) {
|
||||||
|
const baseUrl = configStore.getFileBaseUrl()
|
||||||
|
return `${baseUrl}/api/files/get/${sha256}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewAllImages(currentSha) {
|
||||||
|
const urls = photos.value.map(p => getImageUrl(p.Sha256))
|
||||||
|
const currentIndex = photos.value.findIndex(p => p.Sha256 === currentSha)
|
||||||
|
uni.previewImage({
|
||||||
|
urls: urls,
|
||||||
|
current: currentIndex >= 0 ? currentIndex : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDetail() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await warehouseApi.getItem(itemId.value)
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
item.value = res.data.item
|
||||||
|
photos.value = res.data.photos || []
|
||||||
|
commits.value = res.data.commits || []
|
||||||
|
workOrders.value = res.data.work_orders || []
|
||||||
|
linkedCustomers.value = res.data.customers || []
|
||||||
|
canModify.value = res.data.canModifyItem
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '获取详情失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取物品详情失败', e)
|
||||||
|
uni.showToast({ title: '获取详情失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchContainers() {
|
||||||
|
try {
|
||||||
|
const res = await warehouseApi.listContainer({
|
||||||
|
all_levels: true,
|
||||||
|
entries: 5000,
|
||||||
|
page: 1
|
||||||
|
})
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
// 添加根目录选项,并格式化容器数据
|
||||||
|
const containers = res.data.containers || []
|
||||||
|
containerOptions.value = [
|
||||||
|
{ ID: 0, Title: '根目录' }
|
||||||
|
].concat(containers.map(c => ({
|
||||||
|
ID: c.ID,
|
||||||
|
Title: c.Title
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取容器列表失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMoveModal() {
|
||||||
|
moveModalVisible.value = true
|
||||||
|
fetchContainers()
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMoveModal() {
|
||||||
|
moveModalVisible.value = false
|
||||||
|
selectedContainer.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onContainerChange(e) {
|
||||||
|
selectedContainer.value = containerOptions.value[e.detail.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmMove() {
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: '移动中...' })
|
||||||
|
// ID 为 0 表示移动到根目录
|
||||||
|
const targetId = selectedContainer.value.ID === 0 ? null : selectedContainer.value.ID
|
||||||
|
const res = await warehouseApi.moveItem(itemId.value, targetId)
|
||||||
|
if (res.errCode === 0) {
|
||||||
|
uni.showToast({ title: '移动成功', icon: 'success' })
|
||||||
|
moveModalVisible.value = false
|
||||||
|
selectedContainer.value = {}
|
||||||
|
fetchDetail()
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.errMsg || '移动失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('移动失败', e)
|
||||||
|
uni.showToast({ title: '移动失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editItem() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/warehouse/item-edit?id=${itemId.value}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addWorkOrder() {
|
||||||
|
// 存储预填数据到 storage(参考 web 端实现)
|
||||||
|
const prefillData = {
|
||||||
|
itemId: itemId.value,
|
||||||
|
title: item.value.SerialNumber
|
||||||
|
? `${item.value.Name}-${item.value.SerialNumber}`
|
||||||
|
: item.value.Name,
|
||||||
|
description: item.value.Remark || '',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有已关联的客户,自动填充到工单
|
||||||
|
if (linkedCustomers.value && linkedCustomers.value.length > 0) {
|
||||||
|
prefillData.customer_ids = linkedCustomers.value.map(c => c.ID)
|
||||||
|
prefillData.customers = linkedCustomers.value.map(c => ({
|
||||||
|
id: c.ID,
|
||||||
|
first_name: c.first_name || '',
|
||||||
|
last_name: c.last_name || '',
|
||||||
|
primary_phone: c.primary_phone || ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.setStorageSync('prefill_work_order', JSON.stringify(prefillData))
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/workorder/add-workorder'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToWorkOrder(id) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/workorder/show-workorder?id=${id}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
uni.navigateBack({ delta: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印物品标签
|
||||||
|
function printItem() {
|
||||||
|
if (!printer) {
|
||||||
|
uni.showToast({ title: '打印机插件未加载(仅在 App 环境可用)', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.value) {
|
||||||
|
uni.showToast({ title: '物品信息未加载', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化打印机
|
||||||
|
printer.initPrinter({})
|
||||||
|
printer.setConcentration({ level: 39 })
|
||||||
|
printer.setLineSpacing({ spacing: 1 })
|
||||||
|
|
||||||
|
// 标签打印,使用黑标
|
||||||
|
printer.printEnableMark({
|
||||||
|
enable: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 第一行:标题(名称)
|
||||||
|
printer.setFontSize({ fontSize: 0 })
|
||||||
|
printer.setTextBold({ bold: true })
|
||||||
|
printer.printText({
|
||||||
|
content: (item.value.Name || '物品')+'\n'
|
||||||
|
})
|
||||||
|
printer.setTextBold({ bold: false })
|
||||||
|
//printer.printLine({ line_length: 1 })
|
||||||
|
|
||||||
|
// 第二行:编号
|
||||||
|
if (item.value.SerialNumber) {
|
||||||
|
printer.setFontSize({ fontSize: 1 })
|
||||||
|
printer.printText({
|
||||||
|
content: '序列号:' + item.value.SerialNumber +'\n'
|
||||||
|
})
|
||||||
|
//printer.printLine({ line_length: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第三行:备注
|
||||||
|
if (item.value.Remark) {
|
||||||
|
printer.setFontSize({ fontSize: 1 })
|
||||||
|
printer.printText({
|
||||||
|
content: '备注:' + item.value.Remark+'\n'
|
||||||
|
})
|
||||||
|
//printer.printLine({ line_length: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第四行:创建日期
|
||||||
|
if (item.value.CreatedAt) {
|
||||||
|
printer.setFontSize({ fontSize: 1 })
|
||||||
|
printer.printText({
|
||||||
|
content: '创建日期:' + formatDate(item.value.CreatedAt)
|
||||||
|
})
|
||||||
|
//printer.printLine({ line_length: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 条形码(高度4)
|
||||||
|
printer.printBarcode({
|
||||||
|
text: 'item:' + itemId.value,
|
||||||
|
height: 40,
|
||||||
|
barcodeType: 73
|
||||||
|
})
|
||||||
|
printer.printLine({ line_length: 2 })
|
||||||
|
|
||||||
|
// 提交打印
|
||||||
|
printer.start()
|
||||||
|
|
||||||
|
uni.showToast({ title: '打印成功', icon: 'success' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRefresh() {
|
||||||
|
refreshing.value = true
|
||||||
|
await fetchDetail()
|
||||||
|
refreshing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 备用:在某些情况下 onLoad 可能未触发
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用 onLoad 获取页面参数(推荐方式)
|
||||||
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||||
|
onLoad((options) => {
|
||||||
|
console.log('item-detail onLoad options:', options)
|
||||||
|
if (options && options.id) {
|
||||||
|
itemId.value = parseInt(options.id)
|
||||||
|
fetchDetail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 每次显示页面时刷新数据(从编辑页返回时自动更新)
|
||||||
|
onShow(() => {
|
||||||
|
if (itemId.value > 0) {
|
||||||
|
fetchDetail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding-bottom: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #007AFF;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-btn {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #007AFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: calc(100vh - 180rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading, .empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 20rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 25rpx 30rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
border-bottom: 1rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
width: 150rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.location {
|
||||||
|
color: #007AFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-grid {
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-item {
|
||||||
|
width: calc((100% - 30rpx) / 3);
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 15rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
background-color: #007AFF;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-date {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workorder-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
border-bottom: 1rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workorder-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wo-id {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #007AFF;
|
||||||
|
margin-right: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wo-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wo-status {
|
||||||
|
font-size: 22rpx;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wo-status.pending {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wo-status.checked {
|
||||||
|
background-color: #f3e5f5;
|
||||||
|
color: #9c27b0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.wo-status.parts_ordered {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wo-status.repaired {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wo-status.returned {
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
color: #7a7a7a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wo-status.unrepairable {
|
||||||
|
background-color: #f2bdbe;
|
||||||
|
color: #ff575a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linked-customers {
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linked-customer-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linked-customer-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-title {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||||
|
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 25rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.workorder {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: #fff;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.edit {
|
||||||
|
background-color: #007AFF;
|
||||||
|
color: #fff;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.move {
|
||||||
|
background-color: #ff9800;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗样式 */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 85%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
font-size: 50rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 25rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
border-top: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel, .btn-confirm {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
color: #666;
|
||||||
|
border-right: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
color: #007AFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,597 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="header">
|
||||||
|
<text class="back-btn" @click="goBack">‹ 返回</text>
|
||||||
|
<text class="title">编辑物品</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="content">
|
||||||
|
<view v-if="loading" class="loading">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label required">名称 *</text>
|
||||||
|
<input class="form-input" v-model="form.name" placeholder="请输入物品名称" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">编号</text>
|
||||||
|
<input class="form-input" v-model="form.serialNumber" placeholder="请输入物品编号(选填)" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">数量</text>
|
||||||
|
<view class="quantity-row">
|
||||||
|
<view class="qty-btn" @click="form.quantity > 1 && form.quantity--">
|
||||||
|
<text class="qty-btn-text">−</text>
|
||||||
|
</view>
|
||||||
|
<input class="qty-input" type="number" v-model.number="form.quantity" />
|
||||||
|
<view class="qty-btn" @click="form.quantity++">
|
||||||
|
<text class="qty-btn-text">+</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">备注</text>
|
||||||
|
<textarea class="form-textarea" v-model="form.remark" placeholder="请输入备注(选填)" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关联客户 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">关联客户</text>
|
||||||
|
|
||||||
|
<!-- 已选中的客户 -->
|
||||||
|
<view v-if="selectedCustomers.length > 0" class="selected-customers">
|
||||||
|
<view
|
||||||
|
v-for="customer in selectedCustomers"
|
||||||
|
:key="customer.id"
|
||||||
|
class="customer-tag"
|
||||||
|
>
|
||||||
|
<text>{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</text>
|
||||||
|
<text class="customer-tag-remove" @click="removeCustomer(customer.id)">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<view class="customer-search-box">
|
||||||
|
<input
|
||||||
|
class="customer-search-input"
|
||||||
|
v-model="customerSearchQuery"
|
||||||
|
placeholder="搜索客户..."
|
||||||
|
@input="onCustomerSearchInput"
|
||||||
|
@focus="onCustomerSearchFocus"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 搜索结果 -->
|
||||||
|
<view v-if="showCustomerDropdown && customerSearchResults.length > 0" class="customer-dropdown">
|
||||||
|
<view
|
||||||
|
v-for="customer in customerSearchResults"
|
||||||
|
:key="customer.id"
|
||||||
|
class="customer-dropdown-item"
|
||||||
|
@click="selectCustomer(customer)"
|
||||||
|
>
|
||||||
|
<text class="customer-dropdown-name">{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</text>
|
||||||
|
<text v-if="customer.primary_phone" class="customer-dropdown-phone">{{ customer.primary_phone }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="showCustomerDropdown && customerSearchQuery && customerSearchResults.length === 0 && !customerSearchLoading" class="customer-dropdown-empty">
|
||||||
|
未找到匹配的客户
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">物品图片</text>
|
||||||
|
<view class="image-list">
|
||||||
|
<view
|
||||||
|
v-for="(photo, index) in form.photos"
|
||||||
|
:key="index"
|
||||||
|
class="image-item"
|
||||||
|
>
|
||||||
|
<image class="preview-img" :src="getPhotoUrl(photo)" mode="aspectFill" />
|
||||||
|
<view class="image-remove" @click="removePhoto(index)">×</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="form.photos.length < 9" class="image-add" @click="chooseImage">
|
||||||
|
<text class="add-icon">+</text>
|
||||||
|
<text class="add-text">添加图片</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="submit-section">
|
||||||
|
<view class="submit-btn" @click="submitForm">
|
||||||
|
<text>保存</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { warehouseApi } from '@/api/warehouse.js'
|
||||||
|
import { customerApi } from '@/api/customer.js'
|
||||||
|
import { useConfigStore } from '@/stores/config.js'
|
||||||
|
import api from '@/api/index.js'
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
// 物品ID
|
||||||
|
const itemId = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const form = ref({
|
||||||
|
name: '',
|
||||||
|
serialNumber: '',
|
||||||
|
remark: '',
|
||||||
|
quantity: 1,
|
||||||
|
photos: []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 关联客户
|
||||||
|
const selectedCustomers = ref([])
|
||||||
|
const customerSearchQuery = ref('')
|
||||||
|
const customerSearchResults = ref([])
|
||||||
|
const showCustomerDropdown = ref(false)
|
||||||
|
const customerSearchLoading = ref(false)
|
||||||
|
let customerSearchTimer = null
|
||||||
|
|
||||||
|
function getPhotoUrl(sha256) {
|
||||||
|
const baseUrl = configStore.getFileBaseUrl ? configStore.getFileBaseUrl() : ''
|
||||||
|
return `${baseUrl}/api/files/get/${sha256}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseImage() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 9 - form.value.photos.length,
|
||||||
|
success: (res) => {
|
||||||
|
res.tempFilePaths.forEach(path => {
|
||||||
|
uploadImage(path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImage(filePath) {
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: '上传中...' })
|
||||||
|
const res = await api.upload('/files/upload/image', {
|
||||||
|
uri: filePath,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.errCode === 0 && res.data && res.data.hash) {
|
||||||
|
form.value.photos.push(res.data.hash)
|
||||||
|
uni.showToast({ title: '上传成功', icon: 'success' })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.message || '上传失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('上传失败', e)
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePhoto(index) {
|
||||||
|
form.value.photos.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户搜索
|
||||||
|
async function searchCustomers() {
|
||||||
|
if (!customerSearchQuery.value.trim()) {
|
||||||
|
customerSearchResults.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
customerSearchLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await customerApi.list(1, 20, customerSearchQuery.value)
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
customerSearchResults.value = res.data.customers || res.data || []
|
||||||
|
} else {
|
||||||
|
customerSearchResults.value = []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('搜索客户失败', e)
|
||||||
|
customerSearchResults.value = []
|
||||||
|
} finally {
|
||||||
|
customerSearchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCustomerSearchInput() {
|
||||||
|
clearTimeout(customerSearchTimer)
|
||||||
|
customerSearchTimer = setTimeout(() => {
|
||||||
|
searchCustomers()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCustomerSearchFocus() {
|
||||||
|
showCustomerDropdown.value = true
|
||||||
|
searchCustomers()
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCustomer(customer) {
|
||||||
|
const exists = selectedCustomers.value.find(c => c.id === customer.id)
|
||||||
|
if (!exists) {
|
||||||
|
selectedCustomers.value.push(customer)
|
||||||
|
}
|
||||||
|
customerSearchQuery.value = ''
|
||||||
|
showCustomerDropdown.value = false
|
||||||
|
customerSearchResults.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCustomer(id) {
|
||||||
|
const index = selectedCustomers.value.findIndex(c => c.id === id)
|
||||||
|
if (index >= 0) {
|
||||||
|
selectedCustomers.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDetail() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await warehouseApi.getItem(itemId.value)
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
const item = res.data.item
|
||||||
|
form.value = {
|
||||||
|
name: item.Name || '',
|
||||||
|
serialNumber: item.SerialNumber || '',
|
||||||
|
remark: item.Remark || '',
|
||||||
|
quantity: item.Quantity ?? 1,
|
||||||
|
photos: res.data.photos ? res.data.photos.map(p => p.Sha256) : []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载已关联的客户
|
||||||
|
if (res.data.customers && res.data.customers.length > 0) {
|
||||||
|
selectedCustomers.value = res.data.customers
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '获取详情失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取物品详情失败', e)
|
||||||
|
uni.showToast({ title: '获取详情失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForm() {
|
||||||
|
if (!form.value.name.trim()) {
|
||||||
|
uni.showToast({ title: '请输入名称', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: '保存中...' })
|
||||||
|
const data = {
|
||||||
|
id: itemId.value,
|
||||||
|
name: form.value.name,
|
||||||
|
serial_number: form.value.serialNumber,
|
||||||
|
remark: form.value.remark,
|
||||||
|
quantity: form.value.quantity > 0 ? form.value.quantity : 1,
|
||||||
|
photos: form.value.photos,
|
||||||
|
customer_ids: selectedCustomers.value.map(c => c.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await warehouseApi.updateItem(data)
|
||||||
|
|
||||||
|
if (res.errCode === 0) {
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
goBack()
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.errMsg || '保存失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存失败', e)
|
||||||
|
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取页面参数
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
onLoad((options) => {
|
||||||
|
if (options && options.id) {
|
||||||
|
itemId.value = parseInt(options.id)
|
||||||
|
fetchDetail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #007AFF;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding-right: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: calc(100vh - 120rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 20rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label.required::after {
|
||||||
|
content: ' *';
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
min-height: 150rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片上传 */
|
||||||
|
.image-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item {
|
||||||
|
position: relative;
|
||||||
|
width: 180rpx;
|
||||||
|
height: 180rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0 0 0 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-add {
|
||||||
|
width: 180rpx;
|
||||||
|
height: 180rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 2rpx dashed #ddd;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-icon {
|
||||||
|
font-size: 60rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-section {
|
||||||
|
padding: 40rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background-color: #007AFF;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 数量选择器 */
|
||||||
|
.quantity-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-btn {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-btn-text {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-input {
|
||||||
|
width: 100rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-size: 30rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
margin: 0 16rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关联客户 */
|
||||||
|
.selected-customers {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
border: 1rpx solid #91d5ff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tag-remove {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tag-remove:active {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-box {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 72rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1rpx solid #eee;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
max-height: 400rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown-item:active {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown-phone {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dropdown-empty {
|
||||||
|
padding: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,763 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="header">
|
||||||
|
<text class="back-btn" @click="goBack">‹ 返回</text>
|
||||||
|
<text class="title">新建工单</text>
|
||||||
|
<view class="header-right"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="content">
|
||||||
|
<!-- 表单卡片 -->
|
||||||
|
<view class="card">
|
||||||
|
<!-- 工单标题 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">工单标题 <text class="required">*</text></text>
|
||||||
|
<input
|
||||||
|
v-model="form.title"
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
maxlength="200"
|
||||||
|
placeholder="请输入工单标题"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 问题描述 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">问题描述</text>
|
||||||
|
<textarea
|
||||||
|
v-model="form.description"
|
||||||
|
class="form-textarea"
|
||||||
|
placeholder="请输入问题描述"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关联物品 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">关联物品</text>
|
||||||
|
|
||||||
|
<!-- 已选择物品 -->
|
||||||
|
<view v-if="selectedItems.length > 0" class="selected-items">
|
||||||
|
<view
|
||||||
|
v-for="item in selectedItems"
|
||||||
|
:key="item.ID"
|
||||||
|
class="selected-item-tag"
|
||||||
|
>
|
||||||
|
<view class="selected-item-tag-info">
|
||||||
|
<text class="selected-item-tag-name">{{ item.Name }}</text>
|
||||||
|
<text v-if="item.SerialNumber" class="selected-item-tag-serial">{{ item.SerialNumber }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="remove-item" @click="removeSelectedItem(item.ID)">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<view class="search-box">
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索物品名称或编号"
|
||||||
|
@input="onSearchInput"
|
||||||
|
@focus="onSearchFocus"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 搜索结果下拉 -->
|
||||||
|
<view v-if="showDropdown && searchResults.length > 0" class="dropdown">
|
||||||
|
<view
|
||||||
|
v-for="item in searchResults"
|
||||||
|
:key="item.ID"
|
||||||
|
class="dropdown-item"
|
||||||
|
@click="selectItem(item)"
|
||||||
|
>
|
||||||
|
<view class="dropdown-item-info">
|
||||||
|
<text class="dropdown-name">{{ item.Name }}</text>
|
||||||
|
<text v-if="item.SerialNumber" class="dropdown-serial">{{ item.SerialNumber }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showDropdown && searchLoading" class="dropdown">
|
||||||
|
<view class="dropdown-loading">搜索中...</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showDropdown && searchResults.length === 0 && !searchLoading && searchQuery.trim()" class="dropdown">
|
||||||
|
<view class="dropdown-empty">未找到匹配的物品</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关联客户 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">关联客户</text>
|
||||||
|
|
||||||
|
<!-- 已选择客户 -->
|
||||||
|
<view v-if="selectedCustomers.length > 0" class="selected-customers">
|
||||||
|
<view
|
||||||
|
v-for="customer in selectedCustomers"
|
||||||
|
:key="customer.id"
|
||||||
|
class="selected-customer-tag"
|
||||||
|
>
|
||||||
|
<text class="customer-name">{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</text>
|
||||||
|
<text class="remove-customer" @click="removeSelectedCustomer(customer.id)">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<view class="search-box">
|
||||||
|
<input
|
||||||
|
v-model="customerSearchQuery"
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索客户姓名"
|
||||||
|
@input="onCustomerSearchInput"
|
||||||
|
@focus="onCustomerSearchFocus"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 搜索结果下拉 -->
|
||||||
|
<view v-if="showCustomerDropdown && customerSearchResults.length > 0" class="dropdown">
|
||||||
|
<view
|
||||||
|
v-for="customer in customerSearchResults"
|
||||||
|
:key="customer.id"
|
||||||
|
class="dropdown-item"
|
||||||
|
@click="selectCustomer(customer)"
|
||||||
|
>
|
||||||
|
<text class="dropdown-name">{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</text>
|
||||||
|
<text v-if="customer.primary_phone" class="dropdown-phone">{{ customer.primary_phone }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showCustomerDropdown && customerSearchLoading" class="dropdown">
|
||||||
|
<view class="dropdown-loading">搜索中...</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showCustomerDropdown && customerSearchResults.length === 0 && !customerSearchLoading && customerSearchQuery.trim()" class="dropdown">
|
||||||
|
<view class="dropdown-empty">未找到匹配的客户</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 图片上传 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">图片</text>
|
||||||
|
<view class="photo-upload">
|
||||||
|
<view
|
||||||
|
v-for="(hash, index) in photos"
|
||||||
|
:key="index"
|
||||||
|
class="photo-item"
|
||||||
|
@click="previewImage(index)"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="photo-img"
|
||||||
|
:src="getPhotoUrl(hash)"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="photo-remove" @click.stop="removePhoto(index)">×</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="photos.length < 9" class="photo-add" @click="chooseImage">
|
||||||
|
<text class="photo-add-icon">+</text>
|
||||||
|
<text class="photo-add-text">添加图片</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<view class="submit-bar">
|
||||||
|
<view class="submit-btn" :class="{ disabled: submitting }" @click="submitForm">
|
||||||
|
<text v-if="submitting">提交中...</text>
|
||||||
|
<text v-else>提交工单</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import api from '@/api/index.js'
|
||||||
|
import { workOrderApi } from '@/api/work_order.js'
|
||||||
|
import { warehouseApi } from '@/api/warehouse.js'
|
||||||
|
import { customerApi } from '@/api/customer.js'
|
||||||
|
import { useConfigStore } from '@/stores/config.js'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
const goBack = () => uni.navigateBack()
|
||||||
|
|
||||||
|
// 预填物品ID(从物品详情跳转时)
|
||||||
|
const presetItemIds = ref([])
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
// 优先检查预填数据(从物品详情跳转时)
|
||||||
|
const prefillStr = uni.getStorageSync('prefill_work_order')
|
||||||
|
if (prefillStr) {
|
||||||
|
try {
|
||||||
|
const prefill = JSON.parse(prefillStr)
|
||||||
|
form.title = prefill.title || ''
|
||||||
|
form.description = prefill.description || ''
|
||||||
|
if (prefill.itemId) {
|
||||||
|
presetItemIds.value = [prefill.itemId]
|
||||||
|
fetchPresetItem(prefill.itemId)
|
||||||
|
}
|
||||||
|
// 如果有预填客户信息,自动填充
|
||||||
|
if (prefill.customers && prefill.customers.length > 0) {
|
||||||
|
selectedCustomers.value = prefill.customers.map(c => ({
|
||||||
|
id: c.id,
|
||||||
|
first_name: c.first_name || '',
|
||||||
|
last_name: c.last_name || '',
|
||||||
|
primary_phone: c.primary_phone || ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
uni.removeStorageSync('prefill_work_order')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('读取预填数据失败', e)
|
||||||
|
}
|
||||||
|
} else if (options && options.item_id) {
|
||||||
|
// 兼容旧方式:直接传 item_id 参数
|
||||||
|
presetItemIds.value = [parseInt(options.item_id)]
|
||||||
|
fetchPresetItem(parseInt(options.item_id))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchPresetItem(itemId) {
|
||||||
|
try {
|
||||||
|
const res = await warehouseApi.getItem(itemId)
|
||||||
|
if (res.errCode === 0 && res.data && res.data.item) {
|
||||||
|
const item = res.data.item
|
||||||
|
// 检查是否已选中
|
||||||
|
if (!selectedItems.value.find(i => i.ID === item.ID)) {
|
||||||
|
selectedItems.value.push(item)
|
||||||
|
linkedItemIds.value.push(item.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取物品信息失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const form = reactive({
|
||||||
|
title: '',
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 关联物品
|
||||||
|
const selectedItems = ref([])
|
||||||
|
const linkedItemIds = ref([])
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const searchResults = ref([])
|
||||||
|
const showDropdown = ref(false)
|
||||||
|
const searchLoading = ref(false)
|
||||||
|
let searchTimer = null
|
||||||
|
|
||||||
|
// 关联客户
|
||||||
|
const selectedCustomers = ref([])
|
||||||
|
const customerSearchQuery = ref('')
|
||||||
|
const customerSearchResults = ref([])
|
||||||
|
const showCustomerDropdown = ref(false)
|
||||||
|
const customerSearchLoading = ref(false)
|
||||||
|
let customerSearchTimer = null
|
||||||
|
|
||||||
|
// 图片
|
||||||
|
const photos = ref([])
|
||||||
|
|
||||||
|
// 提交状态
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
function onSearchInput() {
|
||||||
|
clearTimeout(searchTimer)
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
doSearch()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearchFocus() {
|
||||||
|
if (searchQuery.value.trim()) {
|
||||||
|
showDropdown.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doSearch() {
|
||||||
|
if (!searchQuery.value.trim()) {
|
||||||
|
searchResults.value = []
|
||||||
|
showDropdown.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
searchLoading.value = true
|
||||||
|
showDropdown.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await warehouseApi.listItem({
|
||||||
|
search: searchQuery.value.trim()
|
||||||
|
})
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
searchResults.value = (res.data.items || []).slice(0, 10)
|
||||||
|
} else {
|
||||||
|
searchResults.value = []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('搜索物品失败', e)
|
||||||
|
searchResults.value = []
|
||||||
|
} finally {
|
||||||
|
searchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectItem(item) {
|
||||||
|
// 检查是否已选中
|
||||||
|
if (!selectedItems.value.find(i => i.ID === item.ID)) {
|
||||||
|
selectedItems.value.push(item)
|
||||||
|
linkedItemIds.value.push(item.ID)
|
||||||
|
}
|
||||||
|
searchQuery.value = ''
|
||||||
|
searchResults.value = []
|
||||||
|
showDropdown.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSelectedItem(itemId) {
|
||||||
|
selectedItems.value = selectedItems.value.filter(i => i.ID !== itemId)
|
||||||
|
linkedItemIds.value = linkedItemIds.value.filter(id => id !== itemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCustomerSearchInput() {
|
||||||
|
clearTimeout(customerSearchTimer)
|
||||||
|
customerSearchTimer = setTimeout(() => {
|
||||||
|
doCustomerSearch()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCustomerSearchFocus() {
|
||||||
|
if (customerSearchQuery.value.trim()) {
|
||||||
|
showCustomerDropdown.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCustomerSearch() {
|
||||||
|
if (!customerSearchQuery.value.trim()) {
|
||||||
|
customerSearchResults.value = []
|
||||||
|
showCustomerDropdown.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
customerSearchLoading.value = true
|
||||||
|
showCustomerDropdown.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await customerApi.list({
|
||||||
|
search: customerSearchQuery.value.trim(),
|
||||||
|
page: 1,
|
||||||
|
page_size: 10
|
||||||
|
})
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
customerSearchResults.value = (res.data.customers || []).slice(0, 10)
|
||||||
|
} else {
|
||||||
|
customerSearchResults.value = []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('搜索客户失败', e)
|
||||||
|
customerSearchResults.value = []
|
||||||
|
} finally {
|
||||||
|
customerSearchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCustomer(customer) {
|
||||||
|
// 检查是否已选中
|
||||||
|
if (!selectedCustomers.value.find(c => c.id === customer.id)) {
|
||||||
|
selectedCustomers.value.push(customer)
|
||||||
|
}
|
||||||
|
customerSearchQuery.value = ''
|
||||||
|
customerSearchResults.value = []
|
||||||
|
showCustomerDropdown.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSelectedCustomer(customerId) {
|
||||||
|
selectedCustomers.value = selectedCustomers.value.filter(c => c.id !== customerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPhotoUrl(hash) {
|
||||||
|
return configStore.getFileBaseUrl() + '/api/files/get/' + hash
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseImage() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 9 - photos.value.length,
|
||||||
|
success: (res) => {
|
||||||
|
res.tempFilePaths.forEach(path => {
|
||||||
|
uploadImage(path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImage(filePath) {
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: '上传中...' })
|
||||||
|
const res = await api.upload('/files/upload/image', {
|
||||||
|
uri: filePath,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.errCode === 0 && res.data && res.data.hash) {
|
||||||
|
photos.value.push(res.data.hash)
|
||||||
|
uni.showToast({ title: '上传成功', icon: 'success' })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.message || '上传失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('上传失败', e)
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePhoto(index) {
|
||||||
|
photos.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewImage(index) {
|
||||||
|
const urls = photos.value.map(hash => getPhotoUrl(hash))
|
||||||
|
uni.previewImage({
|
||||||
|
current: index,
|
||||||
|
urls: urls
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForm() {
|
||||||
|
if (!form.title.trim()) {
|
||||||
|
uni.showToast({ title: '请输入工单标题', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submitting.value) return
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
title: form.title.trim(),
|
||||||
|
description: form.description.trim(),
|
||||||
|
photos: photos.value,
|
||||||
|
customer_ids: selectedCustomers.value.map(c => c.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linkedItemIds.value.length > 0) {
|
||||||
|
data.item_ids = linkedItemIds.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await workOrderApi.add(data)
|
||||||
|
|
||||||
|
if (res.errCode === 0) {
|
||||||
|
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||||
|
uni.$emit('workorder-refresh')
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.errMsg || '提交失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('提交工单失败', e)
|
||||||
|
uni.showToast({ title: '提交失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #007AFF;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding-right: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
width: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: calc(100vh - 180rpx);
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 200rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-customers {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-customer-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
padding: 15rpx 20rpx;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
border: 1rpx solid #91d5ff;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-phone {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-customer {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #ff4d4f;
|
||||||
|
padding: 5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 90rpx;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
max-height: 400rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 20rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-serial {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-loading,
|
||||||
|
.dropdown-empty {
|
||||||
|
padding: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-upload {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-item {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-add {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 2rpx dashed #d9d9d9;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-add-icon {
|
||||||
|
font-size: 60rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-add-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-items {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10rpx;
|
||||||
|
padding: 15rpx 20rpx;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
border: 1rpx solid #91d5ff;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-tag-info {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-tag-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-tag-serial {
|
||||||
|
display: block;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8c8c8c;
|
||||||
|
margin-top: 5rpx;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-item {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #ff4d4f;
|
||||||
|
padding: 5rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-bar {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background-color: #007AFF;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn.disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,888 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="header">
|
||||||
|
<text class="back-btn" @click="goBack">‹ 返回</text>
|
||||||
|
<text class="title">编辑工单</text>
|
||||||
|
<text class="delete-btn" @click="handleDelete">删除</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="content">
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<view v-if="pageLoading" class="loading">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 错误 -->
|
||||||
|
<view v-else-if="pageError" class="error">
|
||||||
|
<text>{{ pageError }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 表单 -->
|
||||||
|
<view v-else class="card">
|
||||||
|
<!-- 工单标题 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">工单标题 <text class="required">*</text></text>
|
||||||
|
<input
|
||||||
|
v-model="form.title"
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
maxlength="200"
|
||||||
|
placeholder="请输入工单标题"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 问题描述 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">问题描述</text>
|
||||||
|
<textarea
|
||||||
|
v-model="form.description"
|
||||||
|
class="form-textarea"
|
||||||
|
placeholder="请输入问题描述"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关联物品 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">关联物品</text>
|
||||||
|
|
||||||
|
<!-- 已选择物品 -->
|
||||||
|
<view v-if="selectedItems.length > 0" class="selected-items">
|
||||||
|
<view
|
||||||
|
v-for="item in selectedItems"
|
||||||
|
:key="item.ID"
|
||||||
|
class="selected-item-tag"
|
||||||
|
>
|
||||||
|
<view class="selected-item-tag-info">
|
||||||
|
<text class="selected-item-tag-name">{{ item.Name }}</text>
|
||||||
|
<text v-if="item.SerialNumber" class="selected-item-tag-serial">{{ item.SerialNumber }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="remove-item" @click="removeSelectedItem(item.ID)">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<view class="search-box">
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索物品名称或编号"
|
||||||
|
@input="onSearchInput"
|
||||||
|
@focus="onSearchFocus"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 搜索结果下拉 -->
|
||||||
|
<view v-if="showDropdown && searchResults.length > 0" class="dropdown">
|
||||||
|
<view
|
||||||
|
v-for="item in searchResults"
|
||||||
|
:key="item.ID"
|
||||||
|
class="dropdown-item"
|
||||||
|
@click="selectItem(item)"
|
||||||
|
>
|
||||||
|
<view class="dropdown-item-info">
|
||||||
|
<text class="dropdown-name">{{ item.Name }}</text>
|
||||||
|
<text v-if="item.SerialNumber" class="dropdown-serial">{{ item.SerialNumber }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showDropdown && searchLoading" class="dropdown">
|
||||||
|
<view class="dropdown-loading">搜索中...</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showDropdown && searchResults.length === 0 && !searchLoading && searchQuery.trim()" class="dropdown">
|
||||||
|
<view class="dropdown-empty">未找到匹配的物品</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关联客户 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">关联客户</text>
|
||||||
|
|
||||||
|
<!-- 已选择客户 -->
|
||||||
|
<view v-if="selectedCustomers.length > 0" class="selected-customers">
|
||||||
|
<view
|
||||||
|
v-for="customer in selectedCustomers"
|
||||||
|
:key="customer.id"
|
||||||
|
class="selected-customer-tag"
|
||||||
|
>
|
||||||
|
<text class="customer-name">{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</text>
|
||||||
|
<text class="remove-customer" @click="removeSelectedCustomer(customer.id)">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<view class="search-box">
|
||||||
|
<input
|
||||||
|
v-model="customerSearchQuery"
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索客户姓名"
|
||||||
|
@input="onCustomerSearchInput"
|
||||||
|
@focus="onCustomerSearchFocus"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 搜索结果下拉 -->
|
||||||
|
<view v-if="showCustomerDropdown && customerSearchResults.length > 0" class="dropdown">
|
||||||
|
<view
|
||||||
|
v-for="customer in customerSearchResults"
|
||||||
|
:key="customer.id"
|
||||||
|
class="dropdown-item"
|
||||||
|
@click="selectCustomer(customer)"
|
||||||
|
>
|
||||||
|
<text class="dropdown-name">{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</text>
|
||||||
|
<text v-if="customer.primary_phone" class="dropdown-phone">{{ customer.primary_phone }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showCustomerDropdown && customerSearchLoading" class="dropdown">
|
||||||
|
<view class="dropdown-loading">搜索中...</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showCustomerDropdown && customerSearchResults.length === 0 && !customerSearchLoading && customerSearchQuery.trim()" class="dropdown">
|
||||||
|
<view class="dropdown-empty">未找到匹配的客户</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 图片上传 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">图片</text>
|
||||||
|
<view class="photo-upload">
|
||||||
|
<view
|
||||||
|
v-for="(photo, index) in photos"
|
||||||
|
:key="photo.ID || index"
|
||||||
|
class="photo-item"
|
||||||
|
@click="previewPhoto(index)"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="photo-img"
|
||||||
|
:src="getPhotoUrl(photo.Sha256)"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="photo-remove" @click.stop="removePhoto(index)">×</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="photos.length < 9" class="photo-add" @click="chooseImage">
|
||||||
|
<text class="photo-add-icon">+</text>
|
||||||
|
<text class="photo-add-text">添加图片</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<view class="submit-bar">
|
||||||
|
<view class="submit-btn" :class="{ disabled: submitting }" @click="submitForm">
|
||||||
|
<text v-if="submitting">保存中...</text>
|
||||||
|
<text v-else>保存修改</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 删除确认 -->
|
||||||
|
<view v-if="showDeleteConfirm" class="mask" @click="showDeleteConfirm = false">
|
||||||
|
<view class="confirm-dialog" @click.stop>
|
||||||
|
<text class="confirm-title">确认删除</text>
|
||||||
|
<text class="confirm-content">确定要删除这个工单吗?此操作无法撤销。</text>
|
||||||
|
<view class="confirm-btns">
|
||||||
|
<view class="confirm-cancel" @click="showDeleteConfirm = false">取消</view>
|
||||||
|
<view class="confirm-ok" @click="doDelete">删除</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { workOrderApi } from '@/api/work_order.js'
|
||||||
|
import { warehouseApi } from '@/api/warehouse.js'
|
||||||
|
import { customerApi } from '@/api/customer.js'
|
||||||
|
import api from '@/api/index.js'
|
||||||
|
import { useConfigStore } from '@/stores/config.js'
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
const goBack = () => uni.navigateBack()
|
||||||
|
|
||||||
|
// 路由参数
|
||||||
|
let orderId = null
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const pageLoading = ref(true)
|
||||||
|
const pageError = ref('')
|
||||||
|
const submitting = ref(false)
|
||||||
|
const showDeleteConfirm = ref(false)
|
||||||
|
|
||||||
|
// 关联物品
|
||||||
|
const selectedItems = ref([])
|
||||||
|
const linkedItemIds = ref([])
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const searchResults = ref([])
|
||||||
|
const showDropdown = ref(false)
|
||||||
|
const searchLoading = ref(false)
|
||||||
|
let searchTimer = null
|
||||||
|
|
||||||
|
// 关联客户
|
||||||
|
const selectedCustomers = ref([])
|
||||||
|
const customerSearchQuery = ref('')
|
||||||
|
const customerSearchResults = ref([])
|
||||||
|
const showCustomerDropdown = ref(false)
|
||||||
|
const customerSearchLoading = ref(false)
|
||||||
|
let customerSearchTimer = null
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const form = reactive({
|
||||||
|
title: '',
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
const photos = ref([])
|
||||||
|
|
||||||
|
// 获取图片 URL
|
||||||
|
function getPhotoUrl(hash) {
|
||||||
|
return configStore.getFileBaseUrl() + '/api/files/get/' + hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览图片
|
||||||
|
function previewPhoto(index) {
|
||||||
|
const urls = photos.value.map(p => getPhotoUrl(p.Sha256))
|
||||||
|
uni.previewImage({
|
||||||
|
current: index,
|
||||||
|
urls: urls
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 物品搜索
|
||||||
|
function onSearchInput() {
|
||||||
|
clearTimeout(searchTimer)
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
doSearch()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearchFocus() {
|
||||||
|
if (searchQuery.value.trim()) {
|
||||||
|
showDropdown.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doSearch() {
|
||||||
|
if (!searchQuery.value.trim()) {
|
||||||
|
searchResults.value = []
|
||||||
|
showDropdown.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
searchLoading.value = true
|
||||||
|
showDropdown.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await warehouseApi.listItem({
|
||||||
|
search: searchQuery.value.trim()
|
||||||
|
})
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
searchResults.value = (res.data.items || []).slice(0, 10)
|
||||||
|
} else {
|
||||||
|
searchResults.value = []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('搜索物品失败', e)
|
||||||
|
searchResults.value = []
|
||||||
|
} finally {
|
||||||
|
searchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectItem(item) {
|
||||||
|
// 检查是否已选中
|
||||||
|
if (!selectedItems.value.find(i => i.ID === item.ID)) {
|
||||||
|
selectedItems.value.push(item)
|
||||||
|
linkedItemIds.value.push(item.ID)
|
||||||
|
}
|
||||||
|
searchQuery.value = ''
|
||||||
|
searchResults.value = []
|
||||||
|
showDropdown.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSelectedItem(itemId) {
|
||||||
|
selectedItems.value = selectedItems.value.filter(i => i.ID !== itemId)
|
||||||
|
linkedItemIds.value = linkedItemIds.value.filter(id => id !== itemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户搜索
|
||||||
|
function onCustomerSearchInput() {
|
||||||
|
clearTimeout(customerSearchTimer)
|
||||||
|
customerSearchTimer = setTimeout(() => {
|
||||||
|
doCustomerSearch()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCustomerSearchFocus() {
|
||||||
|
if (customerSearchQuery.value.trim()) {
|
||||||
|
showCustomerDropdown.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCustomerSearch() {
|
||||||
|
if (!customerSearchQuery.value.trim()) {
|
||||||
|
customerSearchResults.value = []
|
||||||
|
showCustomerDropdown.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
customerSearchLoading.value = true
|
||||||
|
showCustomerDropdown.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await customerApi.list({
|
||||||
|
search: customerSearchQuery.value.trim(),
|
||||||
|
page: 1,
|
||||||
|
page_size: 10
|
||||||
|
})
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
customerSearchResults.value = (res.data.customers || []).slice(0, 10)
|
||||||
|
} else {
|
||||||
|
customerSearchResults.value = []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('搜索客户失败', e)
|
||||||
|
customerSearchResults.value = []
|
||||||
|
} finally {
|
||||||
|
customerSearchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCustomer(customer) {
|
||||||
|
// 检查是否已选中
|
||||||
|
if (!selectedCustomers.value.find(c => c.id === customer.id)) {
|
||||||
|
selectedCustomers.value.push(customer)
|
||||||
|
}
|
||||||
|
customerSearchQuery.value = ''
|
||||||
|
customerSearchResults.value = []
|
||||||
|
showCustomerDropdown.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSelectedCustomer(customerId) {
|
||||||
|
selectedCustomers.value = selectedCustomers.value.filter(c => c.id !== customerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载工单数据
|
||||||
|
async function fetchOrder() {
|
||||||
|
pageLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await workOrderApi.get(orderId)
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
const order = res.data.order || {}
|
||||||
|
form.title = order.Title || ''
|
||||||
|
form.description = order.Description || ''
|
||||||
|
photos.value = res.data.photos || []
|
||||||
|
|
||||||
|
// 加载已关联的物品
|
||||||
|
if (res.data.linkedItems && res.data.linkedItems.length > 0) {
|
||||||
|
selectedItems.value = res.data.linkedItems
|
||||||
|
linkedItemIds.value = res.data.linkedItems.map(item => item.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载已关联的客户
|
||||||
|
if (res.data.linkedCustomers && res.data.linkedCustomers.length > 0) {
|
||||||
|
selectedCustomers.value = res.data.linkedCustomers
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pageError.value = '工单不存在'
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取工单失败', e)
|
||||||
|
pageError.value = '加载失败'
|
||||||
|
} finally {
|
||||||
|
pageLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择图片
|
||||||
|
function chooseImage() {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 9 - photos.value.length,
|
||||||
|
success: (res) => {
|
||||||
|
res.tempFilePaths.forEach(path => {
|
||||||
|
uploadImage(path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传图片
|
||||||
|
async function uploadImage(filePath) {
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: '上传中...' })
|
||||||
|
const res = await api.upload('/files/upload/image', {
|
||||||
|
uri: filePath,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.errCode === 0 && res.data && res.data.hash) {
|
||||||
|
photos.value.push({ Sha256: res.data.hash })
|
||||||
|
uni.showToast({ title: '上传成功', icon: 'success' })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('上传失败', e)
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除图片
|
||||||
|
function removePhoto(index) {
|
||||||
|
photos.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
async function submitForm() {
|
||||||
|
if (!form.title.trim()) {
|
||||||
|
uni.showToast({ title: '请输入工单标题', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submitting.value) return
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
id: orderId,
|
||||||
|
title: form.title.trim(),
|
||||||
|
description: form.description.trim(),
|
||||||
|
photos: photos.value.map(p => p.Sha256)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加关联物品
|
||||||
|
if (linkedItemIds.value.length > 0) {
|
||||||
|
data.item_ids = linkedItemIds.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加关联客户
|
||||||
|
if (selectedCustomers.value.length > 0) {
|
||||||
|
data.customer_ids = selectedCustomers.value.map(c => c.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await workOrderApi.update(data)
|
||||||
|
|
||||||
|
if (res.errCode === 0) {
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||||
|
uni.$emit('page-refresh')
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.errMsg || '保存失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存失败', e)
|
||||||
|
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除工单
|
||||||
|
function handleDelete() {
|
||||||
|
showDeleteConfirm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doDelete() {
|
||||||
|
try {
|
||||||
|
const res = await workOrderApi.delete(orderId)
|
||||||
|
if (res.errCode === 0) {
|
||||||
|
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||||
|
uni.$emit('page-refresh')
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('删除失败', e)
|
||||||
|
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
showDeleteConfirm.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
const options = currentPage.options || currentPage.$page?.options || {}
|
||||||
|
orderId = options.id ? parseInt(options.id) : null
|
||||||
|
|
||||||
|
if (orderId) {
|
||||||
|
fetchOrder()
|
||||||
|
} else {
|
||||||
|
pageError.value = '工单不存在'
|
||||||
|
pageLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #007AFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: calc(100vh - 120rpx);
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading, .error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 200rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-upload {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-item {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-add {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 2rpx dashed #d9d9d9;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-add-icon {
|
||||||
|
font-size: 60rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-add-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-bar {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background-color: #007AFF;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn.disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除确认弹窗 */
|
||||||
|
.mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog {
|
||||||
|
width: 560rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-content {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-cancel {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 24rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-ok {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 24rpx;
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-items {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10rpx;
|
||||||
|
padding: 15rpx 20rpx;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
border:1rpx solid #91d5ff;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-tag-info {
|
||||||
|
flex:1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-tag-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-tag-serial {
|
||||||
|
display: block;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8c8c8c;
|
||||||
|
margin-top: 5rpx;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-item {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #ff4d4f;
|
||||||
|
padding: 5rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-customers {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-customer-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
padding: 15rpx 20rpx;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
border:1rpx solid #91d5ff;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-phone {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-customer {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #ff4d4f;
|
||||||
|
padding: 5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 90rpx;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
border:1rpx solid #e5e5e5;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
max-height: 400rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 20rpx;
|
||||||
|
border-bottom:1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-serial {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-loading,
|
||||||
|
.dropdown-empty {
|
||||||
|
padding: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="header">
|
||||||
|
<text class="title">工单管理</text>
|
||||||
|
<text class="add-btn" @click="goAddWorkOrder">+ 新建</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="stats-row">
|
||||||
|
<view class="stat-item" @click="filterStatus('pending')">
|
||||||
|
<text class="stat-num">{{ stats.pending || 0 }}</text>
|
||||||
|
<text class="stat-label">待处理</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item" @click="filterStatus('checked')">
|
||||||
|
<text class="stat-num">{{ stats.checked || 0 }}</text>
|
||||||
|
<text class="stat-label">已检查</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item" @click="filterStatus('parts_ordered')">
|
||||||
|
<text class="stat-num">{{ stats.parts_ordered || 0 }}</text>
|
||||||
|
<text class="stat-label">已下单零件</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item" @click="filterStatus('repaired')">
|
||||||
|
<text class="stat-num">{{ stats.repaired || 0 }}</text>
|
||||||
|
<text class="stat-label">已维修</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-bar">
|
||||||
|
<picker mode="selector" :range="statusOptions" range-key="label" @change="onStatusChange">
|
||||||
|
<view class="picker">
|
||||||
|
{{ currentFilter.label || '全部状态' }}
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="order-list" refresher-enabled @refresherrefresh="onRefresh" :refresher-triggered="refreshing" @scrolltolower="loadMore">
|
||||||
|
<view v-if="loading && workOrders.length === 0" class="loading">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="workOrders.length === 0" class="empty">
|
||||||
|
<text>暂无工单</text>
|
||||||
|
</view>
|
||||||
|
<view v-else>
|
||||||
|
<view
|
||||||
|
v-for="item in workOrders"
|
||||||
|
:key="item.ID"
|
||||||
|
class="order-card"
|
||||||
|
@click="goDetail(item.ID)"
|
||||||
|
>
|
||||||
|
<view class="order-header">
|
||||||
|
<text class="order-id">#{{ item.ID }}</text>
|
||||||
|
<text class="order-status" :class="item.CurrentStatus">{{ getStatusText(item.CurrentStatus) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-content">
|
||||||
|
<text class="order-title">{{ item.Title || '无标题' }}</text>
|
||||||
|
<text class="order-desc">{{ item.Description || '无描述' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-footer">
|
||||||
|
<text class="order-date">创建: {{ formatDate(item.CreatedAt) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loadingMore" class="loading-more">
|
||||||
|
<text>加载更多...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="hasMore" class="load-more-btn" @click="loadMore">
|
||||||
|
<text>加载更多</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { workOrderApi } from '@/api/work_order.js'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadingMore = ref(false)
|
||||||
|
const workOrders = ref([])
|
||||||
|
const stats = ref({})
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const hasMore = ref(false)
|
||||||
|
const currentFilter = ref({})
|
||||||
|
const refreshing = ref(false)
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: '', label: '全部状态' },
|
||||||
|
{ value: 'pending', label: '待处理' },
|
||||||
|
{ value: 'checked', label: '已检查' },
|
||||||
|
{ value: 'parts_ordered', label: '已下单零件' },
|
||||||
|
{ value: 'repaired', label: '已维修' },
|
||||||
|
{ value: 'returned', label: '已送还' },
|
||||||
|
{ value: 'unrepairable', label: '无法维修' }
|
||||||
|
]
|
||||||
|
|
||||||
|
function getStatusText(status) {
|
||||||
|
const option = statusOptions.find(s => s.value === status)
|
||||||
|
return option ? option.label : status
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchStats() {
|
||||||
|
try {
|
||||||
|
const res = await workOrderApi.getCount()
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
stats.value = res.data
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取工单统计失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWorkOrders(reset = false) {
|
||||||
|
if (reset) {
|
||||||
|
page.value = 1
|
||||||
|
workOrders.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading.value || loadingMore.value) return
|
||||||
|
if (!reset && !hasMore.value) return
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
loading.value = true
|
||||||
|
} else {
|
||||||
|
loadingMore.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: page.value,
|
||||||
|
entries: pageSize.value
|
||||||
|
}
|
||||||
|
if (currentFilter.value.value) {
|
||||||
|
params.status = currentFilter.value.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await workOrderApi.list(params)
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
const list = res.data.all_orders || []
|
||||||
|
if (reset) {
|
||||||
|
workOrders.value = list
|
||||||
|
} else {
|
||||||
|
workOrders.value = [...workOrders.value, ...list]
|
||||||
|
}
|
||||||
|
hasMore.value = list.length >= pageSize.value
|
||||||
|
page.value++
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取工单列表失败', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStatusChange(e) {
|
||||||
|
currentFilter.value = statusOptions[e.detail.value]
|
||||||
|
fetchWorkOrders(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterStatus(status) {
|
||||||
|
currentFilter.value = statusOptions.find(s => s.value === status) || statusOptions[0]
|
||||||
|
fetchWorkOrders(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMore() {
|
||||||
|
if (hasMore.value && !loadingMore.value) {
|
||||||
|
fetchWorkOrders(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDetail(id) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/workorder/show-workorder?id=${id}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goAddWorkOrder() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/workorder/add-workorder'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRefresh() {
|
||||||
|
refreshing.value = true
|
||||||
|
await Promise.all([fetchStats(), fetchWorkOrders(true)])
|
||||||
|
refreshing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStats()
|
||||||
|
fetchWorkOrders(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听新增/编辑工单后刷新
|
||||||
|
uni.$on('workorder-refresh', () => {
|
||||||
|
fetchStats()
|
||||||
|
fetchWorkOrders(true)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #007AFF;
|
||||||
|
position: absolute;
|
||||||
|
right: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
display: flex;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20rpx 10rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 15rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num {
|
||||||
|
display: block;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #007AFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list {
|
||||||
|
height: calc(100vh - 400rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading, .empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 0 20rpx 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-id {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status {
|
||||||
|
font-size: 22rpx;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.pending {
|
||||||
|
background-color: #fef3c7;
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.checked {
|
||||||
|
background-color: #f3e8ff;
|
||||||
|
color: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.parts_ordered {
|
||||||
|
background-color: #dbeafe;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.repaired {
|
||||||
|
background-color: #dcfce7;
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.returned {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.unrepairable {
|
||||||
|
background-color: #fee2e2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-content {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-footer {
|
||||||
|
border-top: 1rpx solid #f0f0f0;
|
||||||
|
padding-top: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-date {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more, .load-more-btn {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
color: #007AFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-btn {
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 0 20rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 438 B |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 438 B |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 438 B |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 438 B |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 438 B |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 438 B |
@@ -0,0 +1,54 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const useConfigStore = defineStore('config', () => {
|
||||||
|
// API 配置
|
||||||
|
const apiBaseUrl = ref('')
|
||||||
|
|
||||||
|
// 应用配置
|
||||||
|
const appName = ref('OPS')
|
||||||
|
const version = ref('1.0.0')
|
||||||
|
|
||||||
|
// 主题配置
|
||||||
|
const theme = ref('light')
|
||||||
|
|
||||||
|
// 设置 API 地址
|
||||||
|
const setApiBaseUrl = (url) => {
|
||||||
|
apiBaseUrl.value = url
|
||||||
|
uni.setStorageSync('baseUrl', url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getApiBaseUrl=()=>{
|
||||||
|
if(apiBaseUrl.value==='')
|
||||||
|
{
|
||||||
|
apiBaseUrl.value=uni.getStorageSync('baseUrl')
|
||||||
|
}
|
||||||
|
return apiBaseUrl.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片基础 URL(去掉末尾的 /api 部分)
|
||||||
|
const getFileBaseUrl = () => {
|
||||||
|
const base = getApiBaseUrl()
|
||||||
|
if (base) {
|
||||||
|
return base.replace(/\/api$/, '')
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 设置主题
|
||||||
|
const setTheme = (newTheme) => {
|
||||||
|
theme.value = newTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiBaseUrl,
|
||||||
|
appName,
|
||||||
|
version,
|
||||||
|
theme,
|
||||||
|
setApiBaseUrl,
|
||||||
|
getApiBaseUrl,
|
||||||
|
getFileBaseUrl,
|
||||||
|
setTheme
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useConfigStore } from './config'
|
||||||
|
|
||||||
|
// Storage Keys
|
||||||
|
const STORAGE_KEY_COOKIE = 'userCookie'
|
||||||
|
const STORAGE_KEY_COOKIE_SESSION = 'userCookieSession'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载 JSON(永久存储)
|
||||||
|
*/
|
||||||
|
function loadJson(key) {
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除存储
|
||||||
|
*/
|
||||||
|
function removeStorage() {
|
||||||
|
uni.removeStorageSync(STORAGE_KEY_COOKIE)
|
||||||
|
uni.removeStorageSync(STORAGE_KEY_COOKIE_SESSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
// ── State ──
|
||||||
|
const userCookie = ref(null) // Cookie 对象 { Value, Name, ExpiresAt, Remember }
|
||||||
|
const user = ref(null) // 用户基本信息 { ID, Name, ... }
|
||||||
|
const userInfo = ref(null) // 用户详细信息 { Username, FirstName, ... }
|
||||||
|
|
||||||
|
// ── Getters ──
|
||||||
|
/** Cookie 值字符串 */
|
||||||
|
const cookieValue = computed(() => userCookie.value?.Value ?? '')
|
||||||
|
|
||||||
|
/** 是否已登录 */
|
||||||
|
const isLoggedIn = computed(() => !!userCookie.value)
|
||||||
|
|
||||||
|
/** 用户名 */
|
||||||
|
const username = computed(() => {
|
||||||
|
return userInfo.value?.Username || user.value?.Name || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 头像 URL */
|
||||||
|
const avatarUrl = computed(() => {
|
||||||
|
if (userInfo.value?.AvatarPath) {
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
return configStore.getApiBaseUrl() + '/static/avatar/' + userInfo.value.AvatarPath
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Actions ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录 - 保存 Cookie 并获取用户信息
|
||||||
|
* @param {Object} cookie - 后端返回的 cookie 对象
|
||||||
|
*/
|
||||||
|
function login(cookie) {
|
||||||
|
userCookie.value = cookie
|
||||||
|
|
||||||
|
// 持久化存储
|
||||||
|
if (cookie.Remember) {
|
||||||
|
uni.setStorageSync(STORAGE_KEY_COOKIE, JSON.stringify(cookie))
|
||||||
|
}
|
||||||
|
// 会话存储(始终保存)
|
||||||
|
uni.setStorageSync(STORAGE_KEY_COOKIE_SESSION, JSON.stringify(cookie))
|
||||||
|
|
||||||
|
// 检查 cookie 是否过期
|
||||||
|
if (cookie.ExpiresAt && new Date(cookie.ExpiresAt) < new Date()) {
|
||||||
|
logout()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
fetchUserInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
function logout() {
|
||||||
|
userCookie.value = null
|
||||||
|
user.value = null
|
||||||
|
userInfo.value = null
|
||||||
|
removeStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
*/
|
||||||
|
async function fetchUserInfo() {
|
||||||
|
try {
|
||||||
|
const { api } = await import('../api/index.js')
|
||||||
|
const res = await api.post('/users/getinfo', {})
|
||||||
|
if (res.errCode === 0 && res.data) {
|
||||||
|
user.value = res.data.user ?? null
|
||||||
|
userInfo.value = res.data.userInfo ?? null
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取用户信息失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用启动时恢复登录状态
|
||||||
|
*/
|
||||||
|
function restoreSession() {
|
||||||
|
// 优先使用会话存储,否则用永久存储
|
||||||
|
let cookieStr = uni.getStorageSync(STORAGE_KEY_COOKIE_SESSION)
|
||||||
|
if (!cookieStr) {
|
||||||
|
cookieStr = uni.getStorageSync(STORAGE_KEY_COOKIE)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cookieStr) {
|
||||||
|
try {
|
||||||
|
const cookie = JSON.parse(cookieStr)
|
||||||
|
// 检查是否过期
|
||||||
|
if (cookie.ExpiresAt && new Date(cookie.ExpiresAt) < new Date()) {
|
||||||
|
logout()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 直接设置状态并获取用户信息
|
||||||
|
userCookie.value = cookie
|
||||||
|
fetchUserInfo()
|
||||||
|
} catch {
|
||||||
|
logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
userCookie,
|
||||||
|
user,
|
||||||
|
userInfo,
|
||||||
|
// Getters
|
||||||
|
cookieValue,
|
||||||
|
isLoggedIn,
|
||||||
|
username,
|
||||||
|
avatarUrl,
|
||||||
|
// Actions
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
fetchUserInfo,
|
||||||
|
restoreSession
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 用户信息缓存
|
||||||
|
*/
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { usersApi } from '../api/users'
|
||||||
|
|
||||||
|
// 全局缓存
|
||||||
|
const usersInfo = ref({})
|
||||||
|
// 请求中的 promiseMap,防止重复请求
|
||||||
|
const inflightRequests = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID获取用户信息(带缓存)
|
||||||
|
* @param {number} userID
|
||||||
|
* @returns {Promise<{Username, UserEmail} | null>}
|
||||||
|
*/
|
||||||
|
export async function fetchUserInfo(userID) {
|
||||||
|
if (!userID) return null
|
||||||
|
|
||||||
|
// 已有缓存
|
||||||
|
if (usersInfo.value[userID]) {
|
||||||
|
return usersInfo.value[userID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求中,等待完成
|
||||||
|
if (inflightRequests[userID]) {
|
||||||
|
return inflightRequests[userID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发起请求
|
||||||
|
const promise = usersApi.getUserInfoFromUserID(userID).then((res) => {
|
||||||
|
if (res.errCode === 0 && res.raw?.return?.userinfo) {
|
||||||
|
const info = res.raw.return.userinfo
|
||||||
|
usersInfo.value[userID] = info
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}).finally(() => {
|
||||||
|
delete inflightRequests[userID]
|
||||||
|
})
|
||||||
|
|
||||||
|
inflightRequests[userID] = promise
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID获取用户名(同步,需先调用 fetchUserInfo)
|
||||||
|
* @param {number} userID
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getUsername(userID) {
|
||||||
|
if (!userID) return ''
|
||||||
|
const user = usersInfo.value[userID]
|
||||||
|
return user?.Username || `用户${userID}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchUserInfo,
|
||||||
|
getUsername
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
uni.addInterceptor({
|
||||||
|
returnValue (res) {
|
||||||
|
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
res.then((res) => {
|
||||||
|
if (!res) return resolve(res)
|
||||||
|
return res[0] ? reject(res[0]) : resolve(res[1])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 这里是uni-app内置的常用样式变量
|
||||||
|
*
|
||||||
|
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||||
|
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||||
|
*
|
||||||
|
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 颜色变量 */
|
||||||
|
|
||||||
|
/* 行为相关颜色 */
|
||||||
|
$uni-color-primary: #007aff;
|
||||||
|
$uni-color-success: #4cd964;
|
||||||
|
$uni-color-warning: #f0ad4e;
|
||||||
|
$uni-color-error: #dd524d;
|
||||||
|
|
||||||
|
/* 文字基本颜色 */
|
||||||
|
$uni-text-color:#333;//基本色
|
||||||
|
$uni-text-color-inverse:#fff;//反色
|
||||||
|
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||||
|
$uni-text-color-placeholder: #808080;
|
||||||
|
$uni-text-color-disable:#c0c0c0;
|
||||||
|
|
||||||
|
/* 背景颜色 */
|
||||||
|
$uni-bg-color:#ffffff;
|
||||||
|
$uni-bg-color-grey:#f8f8f8;
|
||||||
|
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||||
|
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||||
|
|
||||||
|
/* 边框颜色 */
|
||||||
|
$uni-border-color:#c8c7cc;
|
||||||
|
|
||||||
|
/* 尺寸变量 */
|
||||||
|
|
||||||
|
/* 文字尺寸 */
|
||||||
|
$uni-font-size-sm:12px;
|
||||||
|
$uni-font-size-base:14px;
|
||||||
|
$uni-font-size-lg:16px;
|
||||||
|
|
||||||
|
/* 图片尺寸 */
|
||||||
|
$uni-img-size-sm:20px;
|
||||||
|
$uni-img-size-base:26px;
|
||||||
|
$uni-img-size-lg:40px;
|
||||||
|
|
||||||
|
/* Border Radius */
|
||||||
|
$uni-border-radius-sm: 2px;
|
||||||
|
$uni-border-radius-base: 3px;
|
||||||
|
$uni-border-radius-lg: 6px;
|
||||||
|
$uni-border-radius-circle: 50%;
|
||||||
|
|
||||||
|
/* 水平间距 */
|
||||||
|
$uni-spacing-row-sm: 5px;
|
||||||
|
$uni-spacing-row-base: 10px;
|
||||||
|
$uni-spacing-row-lg: 15px;
|
||||||
|
|
||||||
|
/* 垂直间距 */
|
||||||
|
$uni-spacing-col-sm: 4px;
|
||||||
|
$uni-spacing-col-base: 8px;
|
||||||
|
$uni-spacing-col-lg: 12px;
|
||||||
|
|
||||||
|
/* 透明度 */
|
||||||
|
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||||
|
|
||||||
|
/* 文章场景相关 */
|
||||||
|
$uni-color-title: #2C405A; // 文章标题颜色
|
||||||
|
$uni-font-size-title:20px;
|
||||||
|
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||||
|
$uni-font-size-subtitle:26px;
|
||||||
|
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||||
|
$uni-font-size-paragraph:15px;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* 检查字符串是否是合法的 URL
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isUrl(str) {
|
||||||
|
if (!str || typeof str !== 'string') return false
|
||||||
|
|
||||||
|
// 必须以 http:// 或 https:// 开头(接口地址必须带)
|
||||||
|
const reg = /^https?:\/\/.+/i
|
||||||
|
return reg.test(str.trim())
|
||||||
|
}
|
||||||