first commit

This commit is contained in:
douboer
2026-03-21 18:57:10 +08:00
commit c49aa1a5e9
570 changed files with 107167 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "服务器配置",
"disableScroll": true
}

View File

@@ -0,0 +1,303 @@
<view class="page-root server-settings-page" style="{{themeStyle}}">
<view class="page-content server-settings-content">
<scroll-view class="surface-scroll" scroll-y="true">
<view class="surface-panel server-settings-panel">
<view class="settings-sections">
<view class="settings-section">
<view class="settings-section-head">
<text class="settings-section-title">{{copy.sections.basicTitle}}</text>
<text class="settings-section-desc">{{copy.sections.basicDesc}}</text>
</view>
<view class="field-grid">
<view class="field">
<text>{{copy.fields.name}}</text>
<input class="input" value="{{form.name}}" data-key="name" bindinput="onFieldInput" />
</view>
<view class="field wide">
<text>{{copy.fields.tags}}</text>
<input
class="input"
value="{{tagText}}"
placeholder="{{copy.placeholders.tags}}"
data-key="tagsText"
bindinput="onFieldInput"
/>
</view>
<view class="field">
<text>{{copy.fields.host}}</text>
<input class="input" value="{{form.host}}" data-key="host" bindinput="onFieldInput" />
</view>
<view class="field">
<text>{{copy.fields.port}}</text>
<input class="input" type="number" value="{{form.port}}" data-key="port" bindinput="onFieldInput" />
</view>
<view class="field">
<text>{{copy.fields.username}}</text>
<input class="input" value="{{form.username}}" data-key="username" bindinput="onFieldInput" />
</view>
</view>
</view>
<view class="settings-section">
<view class="settings-section-head">
<text class="settings-section-title">{{copy.sections.authTitle}}</text>
<text class="settings-section-desc">{{copy.sections.authDesc}}</text>
</view>
<view class="field-grid">
<view class="field auth-type-field">
<text>{{copy.fields.authType}}</text>
<view class="auth-type-pills">
<block wx:for="{{authTypeOptions}}" wx:key="value">
<view
class="pill-chip {{authTypeIndex === index ? 'active' : ''}}"
data-index="{{index}}"
bindtap="onAuthTypeTap"
>{{item.label}}</view>
</block>
</view>
</view>
<view wx:if="{{form.authType === 'password'}}" class="field">
<text>{{copy.fields.password}}</text>
<input class="input" password="true" value="{{form.password}}" data-key="password" bindinput="onFieldInput" />
</view>
<view wx:if="{{form.authType === 'privateKey' || form.authType === 'certificate'}}" class="field wide">
<text>{{copy.fields.privateKey}}</text>
<textarea class="textarea" value="{{form.privateKey}}" data-key="privateKey" bindinput="onFieldInput" />
</view>
<view wx:if="{{form.authType === 'privateKey' || form.authType === 'certificate'}}" class="field">
<text>{{copy.fields.passphrase}}</text>
<input class="input" password="true" value="{{form.passphrase}}" data-key="passphrase" bindinput="onFieldInput" />
</view>
<view wx:if="{{form.authType === 'certificate'}}" class="field wide">
<text>{{copy.fields.certificate}}</text>
<textarea class="textarea" value="{{form.certificate}}" data-key="certificate" bindinput="onFieldInput" />
</view>
</view>
</view>
<view class="settings-section">
<view class="settings-section-head">
<text class="settings-section-title">{{copy.sections.connectTitle}}</text>
<text class="settings-section-desc">{{copy.sections.connectDesc}}</text>
</view>
<view class="field-grid">
<view class="field">
<text>{{copy.fields.transportMode}}</text>
<view class="input picker-input readonly-input">gateway</view>
</view>
<view class="field wide ai-workdir-field">
<text>{{copy.fields.aiProjectPath}}</text>
<view class="ai-workdir-wrap">
<view class="ai-workdir-inline">
<input class="input" value="{{form.projectPath}}" placeholder="{{copy.placeholders.aiProjectPath}}" data-key="projectPath" bindinput="onFieldInput" />
<view class="pill-chip ai-workdir-select-btn" bindtap="onOpenDirectoryPicker">{{copy.directoryPicker.openButton}}</view>
</view>
<view wx:if="{{dirPickerVisible}}" class="dir-picker-panel">
<scroll-view class="dir-tree-scroll" scroll-y="true">
<view class="dir-tree-stack">
<view wx:for="{{directoryRows}}" wx:key="path" class="dir-tree-row {{item.isSelected ? 'selected' : ''}}">
<view
class="dir-expand-toggle {{item.canExpand ? '' : 'disabled'}}"
style="margin-left: {{item.paddingLeft}}rpx;"
data-path="{{item.path}}"
catchtap="onDirectoryExpandTap"
>
<text wx:if="{{item.canExpand}}">{{item.expanded ? '▾' : '▸'}}</text>
<text wx:else>·</text>
</view>
<view class="dir-row-main" data-path="{{item.path}}" bindtap="onDirectorySelectTap">
<text class="dir-row-name">{{item.name}}</text>
<text wx:if="{{item.loading}}" class="dir-row-loading">{{copy.directoryPicker.loading}}</text>
</view>
</view>
</view>
</scroll-view>
<text wx:if="{{dirPickerError}}" class="dir-picker-error">{{dirPickerError}}</text>
<view class="actions dir-picker-actions">
<button class="btn" bindtap="onDirectoryPickerCancel">{{copy.directoryPicker.cancel}}</button>
<button class="btn primary" bindtap="onDirectoryPickerConfirm" disabled="{{dirPickerLoading}}">{{copy.directoryPicker.apply}}</button>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="settings-section">
<view class="settings-section-head">
<text class="settings-section-title">{{copy.sections.jumpHostTitle}}</text>
<text class="settings-section-desc">{{copy.sections.jumpHostDesc}}</text>
<switch class="jump-host-switch" checked="{{form.jumpHost.enabled}}" bindchange="onJumpSwitchChange" />
</view>
<view wx:if="{{form.jumpHost.enabled}}" class="field-grid">
<view class="field">
<text>{{copy.fields.jumpHost}}</text>
<input class="input" value="{{form.jumpHost.host}}" data-key="jumpHost.host" bindinput="onFieldInput" />
</view>
<view class="field">
<text>{{copy.fields.jumpPort}}</text>
<input
class="input"
type="number"
value="{{form.jumpHost.port}}"
data-key="jumpHost.port"
bindinput="onFieldInput"
/>
</view>
<view class="field">
<text>{{copy.fields.jumpUsername}}</text>
<input
class="input"
value="{{form.jumpHost.username}}"
data-key="jumpHost.username"
bindinput="onFieldInput"
/>
</view>
<view class="field auth-type-field">
<text>{{copy.fields.authType}}</text>
<view class="auth-type-pills">
<block wx:for="{{authTypeOptions}}" wx:key="value">
<view
class="pill-chip {{form.jumpHost.authType === item.value ? 'active' : ''}}"
data-index="{{index}}"
bindtap="onJumpAuthTypeTap"
>{{item.label}}</view>
</block>
</view>
</view>
<view wx:if="{{form.jumpHost.authType === 'password'}}" class="field">
<text>{{copy.fields.password}}</text>
<input
class="input"
password="true"
value="{{form.jumpPassword}}"
data-key="jumpPassword"
bindinput="onFieldInput"
/>
</view>
<view wx:if="{{form.jumpHost.authType === 'privateKey' || form.jumpHost.authType === 'certificate'}}" class="field wide">
<text>{{copy.fields.privateKey}}</text>
<textarea
class="textarea"
value="{{form.jumpPrivateKey}}"
data-key="jumpPrivateKey"
bindinput="onFieldInput"
/>
</view>
<view wx:if="{{form.jumpHost.authType === 'privateKey' || form.jumpHost.authType === 'certificate'}}" class="field">
<text>{{copy.fields.passphrase}}</text>
<input
class="input"
password="true"
value="{{form.jumpPassphrase}}"
data-key="jumpPassphrase"
bindinput="onFieldInput"
/>
</view>
<view wx:if="{{form.jumpHost.authType === 'certificate'}}" class="field wide">
<text>{{copy.fields.certificate}}</text>
<textarea
class="textarea"
value="{{form.jumpCertificate}}"
data-key="jumpCertificate"
bindinput="onFieldInput"
/>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<view class="bottom-bar server-settings-bottom">
<button
class="icon-btn svg-press-btn"
hover-class="svg-press-btn-hover"
hover-start-time="0"
hover-stay-time="80"
data-press-key="server-settings:back"
disabled="{{!canGoBack}}"
bindtouchstart="onSvgButtonTouchStart"
bindtouchend="onSvgButtonTouchEnd"
bindtouchcancel="onSvgButtonTouchEnd"
bindtap="goBack"
>
<image
class="icon-img svg-press-icon"
src="{{pressedSvgButtonKey === 'server-settings:back' ? (accentIcons.back || icons.back || '/assets/icons/back.svg') : (icons.back || '/assets/icons/back.svg')}}"
mode="aspectFit"
/>
</button>
<view class="bottom-right-actions">
<button
class="icon-btn svg-press-btn"
hover-class="svg-press-btn-hover"
hover-start-time="0"
hover-stay-time="80"
data-press-key="server-settings:connect"
bindtouchstart="onSvgButtonTouchStart"
bindtouchend="onSvgButtonTouchEnd"
bindtouchcancel="onSvgButtonTouchEnd"
bindtap="onConnect"
>
<image
class="icon-img svg-press-icon"
src="{{pressedSvgButtonKey === 'server-settings:connect' ? (accentIcons.connect || icons.connect || '/assets/icons/connect.svg') : (icons.connect || '/assets/icons/connect.svg')}}"
mode="aspectFit"
/>
</button>
<button
class="icon-btn svg-press-btn"
hover-class="svg-press-btn-hover"
hover-start-time="0"
hover-stay-time="80"
data-press-key="server-settings:save"
bindtouchstart="onSvgButtonTouchStart"
bindtouchend="onSvgButtonTouchEnd"
bindtouchcancel="onSvgButtonTouchEnd"
bindtap="onSave"
>
<image
class="icon-img svg-press-icon"
src="{{pressedSvgButtonKey === 'server-settings:save' ? (accentIcons.save || icons.save || '/assets/icons/save.svg') : (icons.save || '/assets/icons/save.svg')}}"
mode="aspectFit"
/>
</button>
<button
class="icon-btn svg-press-btn"
hover-class="svg-press-btn-hover"
hover-start-time="0"
hover-stay-time="80"
data-press-key="server-settings:records"
bindtouchstart="onSvgButtonTouchStart"
bindtouchend="onSvgButtonTouchEnd"
bindtouchcancel="onSvgButtonTouchEnd"
bindtap="onOpenRecords"
>
<image
class="icon-img svg-press-icon"
src="{{pressedSvgButtonKey === 'server-settings:records' ? (accentIcons.recordmanager || icons.recordmanager || '/assets/icons/recordmanager.svg') : (icons.recordmanager || '/assets/icons/recordmanager.svg')}}"
mode="aspectFit"
/>
</button>
<button
class="icon-btn svg-press-btn"
hover-class="svg-press-btn-hover"
hover-start-time="0"
hover-stay-time="80"
data-press-key="server-settings:about"
bindtouchstart="onSvgButtonTouchStart"
bindtouchend="onSvgButtonTouchEnd"
bindtouchcancel="onSvgButtonTouchEnd"
bindtap="onOpenAbout"
>
<image
class="icon-img svg-press-icon"
src="{{pressedSvgButtonKey === 'server-settings:about' ? (accentIcons.about || icons.about || '/assets/icons/about.svg') : (icons.about || '/assets/icons/about.svg')}}"
mode="aspectFit"
/>
</button>
</view>
</view>
</view>

View File

@@ -0,0 +1,282 @@
.server-settings-page {
gap: 0;
}
.server-settings-content {
padding-top: 16rpx;
padding-bottom: 0;
}
.server-settings-panel {
padding-bottom: 16rpx;
gap: 20rpx;
}
.settings-sections {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.settings-section {
border: 1rpx solid var(--surface-border);
background: var(--surface);
border-radius: 16rpx;
padding: 14rpx;
display: flex;
flex-direction: column;
gap: 14rpx;
}
.settings-section-head {
display: flex;
align-items: center;
gap: 12rpx;
flex-wrap: wrap;
}
.jump-host-switch {
margin-left: auto;
}
.settings-section-title {
font-size: 26rpx;
font-weight: 600;
color: var(--text);
}
.settings-section-desc {
font-size: 22rpx;
color: var(--muted);
}
.server-settings-page .field-grid {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.server-settings-page .field {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
gap: 16rpx;
}
.server-settings-page .field.wide {
align-items: flex-start;
}
.server-settings-page .field.auth-type-field {
align-items: flex-start;
}
.server-settings-page .field.ai-workdir-field {
align-items: flex-start;
}
.server-settings-page .field > text {
width: 220rpx;
min-width: 220rpx;
max-width: 220rpx;
margin: 0;
color: var(--muted);
font-size: 22rpx;
line-height: 1.3;
}
.server-settings-page .field.wide > text {
padding-top: 14rpx;
}
.server-settings-page .field .input,
.server-settings-page .field .textarea {
flex: 1;
min-width: 0;
}
.auth-type-pills {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12rpx;
flex-wrap: wrap;
padding: 2rpx 2rpx 4rpx;
}
.pill-chip {
min-height: 52rpx;
padding: 8rpx 18rpx;
border-radius: 999rpx;
border: 1rpx solid var(--btn-border);
background: var(--icon-btn-bg);
color: var(--btn-text);
font-size: 22rpx;
line-height: 1.1;
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
font-weight: 500;
letter-spacing: 0.2rpx;
transition:
background 140ms ease,
border-color 140ms ease,
color 140ms ease,
box-shadow 140ms ease;
}
.pill-chip.active {
border-color: var(--accent-border);
background: var(--accent-bg-strong);
color: var(--text);
font-weight: 700;
box-shadow:
0 0 0 2rpx var(--accent-ring),
0 8rpx 18rpx var(--accent-shadow);
}
.ai-workdir-wrap {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.ai-workdir-inline {
display: flex;
align-items: center;
gap: 12rpx;
}
.ai-workdir-inline .input {
flex: 1;
min-width: 0;
}
.ai-workdir-select-btn {
flex: 0 0 auto;
min-height: 64rpx;
padding: 10rpx 18rpx;
}
.dir-picker-panel {
border: 1rpx solid rgba(141, 187, 255, 0.26);
border-radius: 14rpx;
background: rgba(10, 20, 36, 0.58);
padding: 12rpx;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.dir-tree-scroll {
max-height: 360rpx;
border-radius: 12rpx;
border: 1rpx solid rgba(141, 187, 255, 0.2);
background: rgba(7, 14, 24, 0.35);
}
.dir-tree-stack {
display: flex;
flex-direction: column;
gap: 4rpx;
padding: 8rpx;
}
.dir-tree-row {
display: flex;
align-items: center;
border-radius: 10rpx;
min-height: 56rpx;
}
.dir-tree-row.selected {
background: rgba(91, 210, 255, 0.16);
box-shadow: inset 0 0 0 1rpx rgba(113, 168, 235, 0.48);
}
.dir-expand-toggle {
width: 44rpx;
height: 44rpx;
margin-right: 6rpx;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--muted);
font-size: 22rpx;
flex: 0 0 auto;
}
.dir-expand-toggle.disabled {
opacity: 0.35;
}
.dir-row-main {
flex: 1;
min-width: 0;
display: inline-flex;
align-items: center;
gap: 10rpx;
padding-right: 8rpx;
}
.dir-row-name {
flex: 1;
min-width: 0;
color: var(--text);
font-size: 22rpx;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dir-row-loading {
flex: 0 0 auto;
color: var(--muted);
font-size: 20rpx;
}
.dir-picker-error {
color: var(--danger);
font-size: 21rpx;
line-height: 1.4;
}
.dir-picker-actions {
justify-content: flex-end;
}
.picker-input {
display: flex;
align-items: center;
}
.readonly-input {
opacity: 0.75;
}
.server-settings-page .textarea {
min-height: 180rpx;
line-height: 1.45;
}
.server-settings-bottom {
padding: 0 64rpx 0 32rpx;
}
.server-settings-bottom .svg-press-btn {
--svg-press-active-radius: 999rpx;
--svg-press-active-bg: rgba(156, 169, 191, 0.24);
--svg-press-active-shadow:
inset 0 0 0 1rpx rgba(210, 220, 236, 0.34),
0 0 0 8rpx rgba(156, 169, 191, 0.12);
--svg-press-active-scale: 0.9;
--svg-press-icon-opacity: 0.96;
--svg-press-icon-active-opacity: 0.68;
--svg-press-icon-active-scale: 0.88;
}