This commit is contained in:
王子琦
2026-01-09 15:48:50 +08:00
commit ea34c396dd
106 changed files with 60207 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

17
mobile-h5/.eslintrc.js Normal file
View File

@@ -0,0 +1,17 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'eslint:recommended'
],
parserOptions: {
parser: '@babel/eslint-parser'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}

23
mobile-h5/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
mobile-h5/README.md Normal file
View File

@@ -0,0 +1,24 @@
# mobile-h5
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

19
mobile-h5/jsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

12206
mobile-h5/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
mobile-h5/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "mobile-h5",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"lib-flexible": "^0.3.2",
"vant": "^2.13.9",
"vue": "^2.6.14",
"vue-router": "^3.5.1"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"postcss-pxtorem": "^6.1.0",
"vue-template-compiler": "^2.6.14"
}
}

View File

@@ -0,0 +1,11 @@
module.exports = {
plugins: {
autoprefixer: {},
'postcss-pxtorem': {
rootValue: 37.5,
propList: ['*'],
selectorBlackList: ['.ignore-px'],
minPixelValue: 2
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no"
>
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

12
mobile-h5/src/App.vue Normal file
View File

@@ -0,0 +1,12 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<style>
#app {
min-height: 100vh;
background-color: #f7f8fa;
}
</style>

View File

@@ -0,0 +1,72 @@
const BASE_URL = '/api/registrations'
/**
* 获取所有注册记录
*/
export async function getRegistrations() {
const response = await fetch(BASE_URL)
if (!response.ok) {
throw new Error('获取注册列表失败')
}
return response.json()
}
/**
* 根据ID获取单条注册记录
* @param {number} id
*/
export async function getRegistrationById(id) {
const response = await fetch(`${BASE_URL}/${id}`)
if (response.status === 404) {
return null
}
if (!response.ok) {
throw new Error('获取注册记录失败')
}
return response.json()
}
/**
* 创建注册记录
* @param {object} registration
*/
export async function createRegistration(registration) {
const response = await fetch(BASE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(registration)
})
if (!response.ok) {
throw new Error('创建注册记录失败')
}
return response.json()
}
/**
* 更新注册记录
* @param {number} id
* @param {object} registration
*/
export async function updateRegistration(id, registration) {
const response = await fetch(`${BASE_URL}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(registration)
})
return response.ok
}
/**
* 删除注册记录
* @param {number} id
*/
export async function deleteRegistration(id) {
const response = await fetch(`${BASE_URL}/${id}`, {
method: 'DELETE'
})
return response.ok
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,59 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

16
mobile-h5/src/main.js Normal file
View File

@@ -0,0 +1,16 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Vant from 'vant'
import 'vant/lib/index.css'
import 'lib-flexible/flexible'
import './styles/global.css'
Vue.config.productionTip = false
Vue.use(Vant)
new Vue({
router,
render: h => h(App)
}).$mount('#app')

View File

@@ -0,0 +1,25 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import RegistrationList from '../views/RegistrationList.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/registrations',
name: 'registrations',
component: RegistrationList
}
]
const router = new VueRouter({
routes
})
export default router

View File

@@ -0,0 +1,27 @@
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif;
background-color: #f7f8fa;
color: #323233;
}
a {
color: inherit;
text-decoration: none;
}
.page-container {
min-height: 100vh;
background-color: #f7f8fa;
/* padding: 12px 12px 64px; */
box-sizing: border-box;
}
.van-cell-group--inset{
padding: 0;
}

View File

@@ -0,0 +1,5 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@@ -0,0 +1,94 @@
<template>
<div class="home page-container">
<van-nav-bar
title="Vant H5 模板"
left-text="返回"
left-arrow
fixed
placeholder
@click-left="onBack"
/>
<div class="hero-card">
<div class="hero-title">Vue 2 + Vant</div>
<div class="hero-subtitle">移动端开箱即用的 UI 模板</div>
<van-button type="primary" block round @click="onAction">立即体验</van-button>
</div>
<van-grid :border="false" column-num="3" clickable>
<van-grid-item icon="fire-o" text="高质量组件" />
<van-grid-item icon="guide-o" text="移动端导航" />
<van-grid-item icon="balance-o" text="主题可定制" />
</van-grid>
<van-cell-group inset title="快速入口">
<van-cell title="登记列表" icon="orders-o" is-link to="/registrations" />
<van-cell title="表单示例" icon="todo-list-o" is-link @click="onNav('表单示例')" />
</van-cell-group>
<van-divider>更多组件请查看 Vant 文档</van-divider>
<van-tabbar v-model="activeTab" fixed>
<van-tabbar-item icon="home-o">首页</van-tabbar-item>
<van-tabbar-item icon="friends-o">社区</van-tabbar-item>
<van-tabbar-item icon="setting-o">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
export default {
name: 'HomeView',
data () {
return {
activeTab: 0
}
},
methods: {
onBack () {
if (window.history.length > 1) {
this.$router.back()
}
},
onAction () {
this.$toast('Vant 已就绪')
},
onNav (name) {
this.$dialog.alert({
title: '示例入口',
message: `${name} 可在此扩展`
})
}
}
}
</script>
<style scoped>
.home {
padding-top: 16px;
}
.hero-card {
background: linear-gradient(135deg, #3f8cff, #65c5ff);
color: #fff;
border-radius: 12px;
padding: 20px 16px;
box-shadow: 0 10px 30px rgba(63, 140, 255, 0.16);
margin-bottom: 16px;
}
.hero-title {
font-size: 20px;
font-weight: 700;
margin-bottom: 6px;
}
.hero-subtitle {
opacity: 0.9;
margin-bottom: 14px;
}
.van-grid {
margin-bottom: 12px;
}
</style>

View File

@@ -0,0 +1,353 @@
<template>
<div class="registration-list page-container">
<van-nav-bar
title="登记列表"
left-text=""
fixed
placeholder
@click-left="onBack"
/>
<van-pull-refresh
v-model="refreshing"
class="list-wrapper"
@refresh="onRefresh"
>
<template #loading><div style="height:0"/></template>
<van-list
v-model="loading"
:finished="finished"
:loading-text="refreshing ? '' : '加载中...'"
finished-text="没有更多了"
@load="loadData"
>
<div v-for="item in list" :key="item.id" class="card-item">
<div class="card-header">
<div class="card-title">
<div class="name">{{ item.customerName }}</div>
<div class="time">{{ item.registerTime }}</div>
</div>
<van-tag :type="getStatusType(item.status)" class="status-tag">
{{ item.status }}
</van-tag>
</div>
<div class="card-body">
<div class="info-row">
<span class="label">联系方式</span>
<span class="value">{{ item.contact || '-' }}</span>
</div>
<template v-if="isLocked(item)">
<div class="info-row">
<span class="label">服务类型</span>
<span class="value">{{ item.serviceType || '-' }}</span>
</div>
<div class="info-row">
<span class="label">电源类型</span>
<span class="value">{{ item.powerType || '-' }}</span>
</div>
<div class="info-row">
<span class="label">备注</span>
<span class="value">{{ item.note || '-' }}</span>
</div>
</template>
<template v-else>
<van-field
v-model.trim="item.serviceType"
label="服务类型"
placeholder="请选择服务类型"
input-align="right"
is-link
border
class="editable-field"
@click="openPicker(item, 'serviceType')"
/>
<van-field
v-model.trim="item.powerType"
label="电源类型"
placeholder="请选择电源类型"
input-align="right"
is-link
border
class="editable-field"
@click="openPicker(item, 'powerType')"
/>
<van-field
v-model.trim="item.note"
type="textarea"
label="备注"
placeholder="请输入备注"
autosize
border
class="editable-field"
/>
</template>
</div>
<div class="card-actions" v-if="!isLocked(item)">
<van-button
type="primary"
size="small"
round
class="submit-btn"
@click="submitItem(item)"
>
登记完成
</van-button>
</div>
</div>
<van-empty v-if="!loading && list.length === 0" description="暂无登记记录" />
</van-list>
</van-pull-refresh>
<van-popup v-model="showPicker" position="bottom" round>
<van-picker
show-toolbar
:columns="pickerColumns"
@confirm="onPickerConfirm"
@cancel="showPicker = false"
/>
</van-popup>
</div>
</template>
<script>
import { getRegistrations, updateRegistration } from '@/api/registration'
import { Toast,Dialog } from 'vant';
export default {
name: 'RegistrationList',
data() {
return {
list: [],
loading: false,
finished: false,
refreshing: true,
showPicker: false,
pickerColumns: [],
currentItem: null,
currentField: '',
serviceOptions: ['安装', '维修', '咨询'],
powerOptions: ['市电', '锂电', '其他']
}
},
methods: {
onBack() {
if (window.history.length > 1) {
this.$router.back()
} else {
this.$router.push('/')
}
},
async loadData() {
try {
const data = await getRegistrations()
this.list = data
setTimeout(()=>{
this.finished = true
},500)
} catch (error) {
this.$toast.fail('加载失败')
console.error(error)
} finally {
this.loading = false
this.refreshing = false
}
},
async onRefresh() {
this.finished = false
this.list = []
await this.loadData()
},
getStatusType(status) {
const map = {
'已登记': 'success',
'待登记': 'primary',
'处理中': 'primary',
'已完成': 'success',
'已取消': 'default'
}
return map[status] || 'warning'
},
isLocked(item = {}) {
return item.status === '已登记'
},
async submitItem(item) {
try {
const confirmed = await Dialog.confirm({
title: '确认提交',
message: '提交后将锁定该记录,是否继续?'
}).then(() => true).catch(() => false)
if (!confirmed) return
Toast.loading()
item.status = '已登记'
const success = await updateRegistration(item.id, item)
Toast.clear()
if (success) {
this.$toast.success('提交成功')
} else {
this.$toast.fail('提交失败')
}
} catch (error) {
this.$toast.fail('提交失败')
console.error(error)
}
},
openPicker(item, field) {
if (this.isLocked(item)) return
this.currentItem = item
this.currentField = field
this.pickerColumns = field === 'serviceType' ? this.serviceOptions : this.powerOptions
this.showPicker = true
},
onPickerConfirm(value) {
if (this.isLocked(this.currentItem)) {
this.showPicker = false
return
}
if (this.currentItem && this.currentField) {
this.$set(this.currentItem, this.currentField, value)
}
this.showPicker = false
}
}
}
</script>
<style scoped>
.registration-list {
background: #f7f8fa;
min-height: 100vh;
padding-top: 16px;
padding-bottom: 20px;
display: flex;
flex-direction: column;
}
.list-wrapper {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
padding: 0 12px;
box-sizing: border-box;
}
.card-item {
margin-top: 12px;
padding: 14px 14px 10px;
border-radius: 14px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
background: #fff;
}
.card-item:first-of-type {
margin-top: 0;
}
.card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.card-title {
flex: 1;
}
.name {
font-size: 16px;
font-weight: 700;
color: #303133;
line-height: 1.3;
}
.time {
margin-top: 4px;
font-size: 12px;
color: #909399;
}
.status-tag {
border-radius: 12px;
padding: 3px 12px;
font-size: 12px;
}
.card-body {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
display: grid;
gap: 6px;
}
.card-actions {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
.submit-btn {
padding: 0 18px;
width: 100%;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
color: #303133;
}
.label {
color: #909399;
}
.value {
max-width: 70%;
text-align: right;
word-break: break-all;
}
.info-row{
padding: 6px 0;
}
:deep(.editable-field){
padding: 6px 0;
}
:deep(.editable-field .van-field__control) {
font-size: 14px;
color: #303133;
padding: 0 !important;
}
:deep(.editable-field .van-field__control--textarea) {
min-height: 80px;
line-height: 1.6;
padding: 10px 12px !important;
background: #f2f3f5;
border: 1px solid #e6e8eb;
border-radius: 10px;
}
:deep(.editable-field .van-field__body) {
align-items: flex-start;
}
:deep(.editable-field .van-field__control::placeholder) {
color: #c0c4cc;
}
:deep(.editable-field .van-field__label) {
color: #909399;
}
:deep(.editable-field .van-field__right-icon) {
color: #c0c4cc;
}
</style>

12
mobile-h5/vue.config.js Normal file
View File

@@ -0,0 +1,12 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8088',
changeOrigin: true
}
}
}
})